Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3953d581c3 |
208
api.php
Normal file
208
api.php
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'User not authenticated']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = isset($_POST['action']) ? $_POST['action'] : ($_GET['action'] ?? '');
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'get_new_messages':
|
||||
getNewMessages();
|
||||
break;
|
||||
case 'send_message':
|
||||
sendMessage();
|
||||
break;
|
||||
case 'create_room':
|
||||
createRoom();
|
||||
break;
|
||||
case 'start_private_chat':
|
||||
startPrivateChat();
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Invalid action.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
global $current_user_id;
|
||||
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$room_id = $_POST['room_id'] ?? null;
|
||||
|
||||
if (empty($message) && empty($_FILES['attachment']['name'])) {
|
||||
throw new Exception("Message or attachment cannot be empty.");
|
||||
}
|
||||
if (!$room_id) {
|
||||
throw new Exception("Invalid room.");
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$has_attachment = false;
|
||||
$file_data = null;
|
||||
|
||||
// Handle file upload
|
||||
if (isset($_FILES['attachment']) && $_FILES['attachment']['error'] == UPLOAD_ERR_OK) {
|
||||
$file = $_FILES['attachment'];
|
||||
if ($file['size'] > 100 * 1024 * 1024) { // 100MB limit
|
||||
throw new Exception("File size exceeds the 100MB limit.");
|
||||
}
|
||||
|
||||
$upload_dir = 'uploads/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0775, true);
|
||||
}
|
||||
$file_name = uniqid() . '-' . basename($file['name']);
|
||||
$file_path = $upload_dir . $file_name;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $file_path)) {
|
||||
$has_attachment = true;
|
||||
$file_data = [
|
||||
'name' => $file['name'],
|
||||
'path' => $file_path,
|
||||
'size' => $file['size'],
|
||||
'type' => $file['type']
|
||||
];
|
||||
} else {
|
||||
throw new Exception("Failed to upload file.");
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO messages (user_id, room_id, message) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$current_user_id, $room_id, $message]);
|
||||
$message_id = $pdo->lastInsertId();
|
||||
|
||||
if ($has_attachment && $file_data) {
|
||||
$stmt = $pdo->prepare("INSERT INTO files (message_id, original_name, file_path, file_size, mime_type) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$message_id, $file_data['name'], $file_data['path'], $file_data['size'], $file_data['type']]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
|
||||
|
||||
function getNewMessages() {
|
||||
global $current_user_id;
|
||||
$roomId = $_GET['room_id'] ?? 0;
|
||||
$lastMessageId = $_GET['last_message_id'] ?? 0;
|
||||
|
||||
if (empty($roomId)) {
|
||||
echo json_encode([]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Check if user is a member of the room
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM room_members WHERE room_id = ? AND user_id = ?");
|
||||
$stmt->execute([$roomId, $current_user_id]);
|
||||
if ($stmt->fetchColumn() === false) {
|
||||
// If not a member, check if it's a public room (no members defined)
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM room_members WHERE room_id = ?");
|
||||
$stmt->execute([$roomId]);
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
http_response_code(403);
|
||||
throw new Exception("Access denied to room");
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for new messages
|
||||
$startTime = time();
|
||||
$timeout = 25; // 25 seconds timeout for long polling
|
||||
|
||||
while (time() - $startTime < $timeout) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
m.id, m.message, m.created_at, u.username,
|
||||
f.original_name as file_name, f.file_path, f.file_size, f.mime_type as file_type
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
LEFT JOIN files f ON m.id = f.message_id
|
||||
WHERE m.room_id = ? AND m.id > ?
|
||||
ORDER BY m.id ASC
|
||||
");
|
||||
$stmt->execute([$roomId, $lastMessageId]);
|
||||
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($messages)) {
|
||||
echo json_encode($messages);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Wait for a short period before polling again
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// If no new messages after timeout, return empty array
|
||||
echo json_encode([]);
|
||||
}
|
||||
|
||||
function createRoom() {
|
||||
global $current_user_id;
|
||||
$room_name = trim($_POST['room_name'] ?? '');
|
||||
if (empty($room_name)) {
|
||||
throw new Exception("Room name cannot be empty.");
|
||||
}
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("INSERT INTO rooms (name, created_by) VALUES (?, ?)");
|
||||
$stmt->execute([$room_name, $current_user_id]);
|
||||
$new_room_id = $pdo->lastInsertId();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO room_members (room_id, user_id) VALUES (?, ?)");
|
||||
$stmt->execute([$new_room_id, $current_user_id]);
|
||||
|
||||
echo json_encode(['success' => true, 'room_id' => $new_room_id]);
|
||||
}
|
||||
|
||||
function startPrivateChat() {
|
||||
global $current_user_id;
|
||||
$other_user_id = $_POST['user_id'] ?? null;
|
||||
if (!$other_user_id || $other_user_id == $current_user_id) {
|
||||
throw new Exception("Invalid user ID.");
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
// Check if a private room already exists between the two users
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT r.id FROM rooms r
|
||||
JOIN room_members rm1 ON r.id = rm1.room_id
|
||||
JOIN room_members rm2 ON r.id = rm2.room_id
|
||||
WHERE r.is_private = 1
|
||||
AND rm1.user_id = ?
|
||||
AND rm2.user_id = ?
|
||||
");
|
||||
$stmt->execute([$current_user_id, $other_user_id]);
|
||||
$room = $stmt->fetch();
|
||||
|
||||
if ($room) {
|
||||
echo json_encode(['success' => true, 'room_id' => $room['id']]);
|
||||
} else {
|
||||
// Create a new private room
|
||||
$stmt = $pdo->prepare("INSERT INTO rooms (name, created_by, is_private) VALUES (?, ?, 1)");
|
||||
$stmt->execute(["Private Chat", $current_user_id]);
|
||||
$new_room_id = $pdo->lastInsertId();
|
||||
|
||||
// Add both users to the new room
|
||||
$stmt = $pdo->prepare("INSERT INTO room_members (room_id, user_id) VALUES (?, ?), (?, ?)");
|
||||
$stmt->execute([$new_room_id, $current_user_id, $new_room_id, $other_user_id]);
|
||||
|
||||
echo json_encode(['success' => true, 'room_id' => $new_room_id]);
|
||||
}
|
||||
}
|
||||
584
index.php
584
index.php
@ -1,150 +1,452 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
// If the user is not logged in, redirect to login page
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
$current_username = $_SESSION['username'];
|
||||
$pdo = db();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Fetch all users for the contact list
|
||||
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE id != ? ORDER BY username ASC");
|
||||
$stmt->execute([$current_user_id]);
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
// Fetch all rooms the user is a member of
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
r.id,
|
||||
r.name,
|
||||
r.is_private,
|
||||
(SELECT u.username FROM users u JOIN room_members rm_other ON u.id = rm_other.user_id WHERE rm_other.room_id = r.id AND rm_other.user_id != ?) AS private_chat_partner
|
||||
FROM rooms r
|
||||
JOIN room_members rm ON r.id = rm.room_id
|
||||
WHERE rm.user_id = ?
|
||||
ORDER BY r.is_private, r.name ASC"
|
||||
);
|
||||
$stmt->execute([$current_user_id, $current_user_id]);
|
||||
$rooms = $stmt->fetchAll();
|
||||
|
||||
|
||||
// If user has no rooms, create a "General" one and add them to it
|
||||
if (empty($rooms)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO rooms (name, created_by) VALUES ('General', ?)");
|
||||
$stmt->execute([$current_user_id]);
|
||||
$general_room_id = $pdo->lastInsertId();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO room_members (room_id, user_id) VALUES (?, ?)");
|
||||
$stmt->execute([$general_room_id, $current_user_id]);
|
||||
|
||||
// Re-fetch rooms
|
||||
$stmt->execute([$current_user_id, $current_user_id]);
|
||||
$rooms = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// Determine the current room
|
||||
$current_room_id = $_GET['room_id'] ?? $rooms[0]['id'] ?? null;
|
||||
$current_room = null;
|
||||
if ($current_room_id) {
|
||||
foreach ($rooms as $room) {
|
||||
if ($room['id'] == $current_room_id) {
|
||||
$current_room = $room;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is not a member of the requested room, redirect to their first room
|
||||
if ($current_room_id && !$current_room) {
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Messages are now fetched by the frontend
|
||||
$messages = [];
|
||||
$last_message_id = 0;
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>AD Messaging App</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: { extend: { colors: { "primary": "#005A9C", "primary-light": "#e6f3ff", "background-light": "#f4f7fa", "background-dark": "#101922", "text-light": "#212529", "text-dark": "#f4f7fa", "border-light": "#dee2e6", "border-dark": "#324d67", "placeholder-light": "#6c757d", "placeholder-dark": "#92adc9", "input-bg-light": "#ffffff", "input-bg-dark": "#192633", "accent-blue-hover": "#007bff", "success": "#28a745", "warning": "#ffc107", "danger": "#dc3545", "info": "#17a2b8", "sidebar-bg": "#ffffff", "sidebar-bg-dark": "#0f1419" }, fontFamily: { "display": ["Inter", "sans-serif"] } } }
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
.dark ::-webkit-scrollbar-track { background: #192633; }
|
||||
.dark ::-webkit-scrollbar-thumb { background: #324d67; }
|
||||
html:not(.dark) ::-webkit-scrollbar-thumb { background: #ced4da; }
|
||||
[x-cloak] { display: none !important; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
</style>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<body class="bg-background-light dark:bg-background-dark font-display text-text-light dark:text-text-dark">
|
||||
|
||||
<div class="flex h-screen text-sm">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-80 flex-shrink-0 bg-white dark:bg-background-dark border-r border-border-light dark:border-border-dark flex flex-col">
|
||||
<div class="p-4 border-b border-border-light dark:border-border-dark flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold">Messages</h2>
|
||||
<div x-data="themeSwitcher()" class="relative">
|
||||
<button @click="open = !open" class="p-2 text-gray-500 rounded-lg dark:text-gray-400 hover:bg-primary-light dark:hover:bg-input-bg-dark hover:text-primary dark:hover:text-white" title="Settings"><span class="material-symbols-outlined">settings</span></button>
|
||||
<div x-show="open" @click.away="open = false" x-cloak class="absolute top-12 right-0 w-48 bg-white dark:bg-input-bg-dark rounded-lg shadow-lg border dark:border-border-dark py-1 z-10">
|
||||
<p class="px-4 py-2 text-xs text-gray-400">Theme</p>
|
||||
<button @click="setTheme('light')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">light_mode</span> Light</button>
|
||||
<button @click="setTheme('dark')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">dark_mode</span> Dark</button>
|
||||
<button @click="setTheme('system')" class="w-full flex items-center px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700"><span class="material-symbols-outlined mr-2">desktop_windows</span> System</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="{ tab: 'rooms' }" class="flex-1 flex flex-col">
|
||||
<div class="border-b border-border-light dark:border-border-dark">
|
||||
<nav class="flex -mb-px">
|
||||
<button @click="tab = 'rooms'" :class="{ 'border-primary text-primary': tab === 'rooms', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': tab !== 'rooms' }" class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none">Rooms</button>
|
||||
<button @click="tab = 'users'" :class="{ 'border-primary text-primary': tab === 'users', 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300': tab !== 'users' }" class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none">Users</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- Rooms Tab -->
|
||||
<div x-show="tab === 'rooms'" class="tab-content active">
|
||||
<?php foreach ($rooms as $room):
|
||||
$room_name = $room['is_private'] ? $room['private_chat_partner'] : $room['name'];
|
||||
$avatar_text = $room['is_private'] ? strtoupper(substr($room['private_chat_partner'], 0, 1)) : strtoupper(substr($room['name'], 0, 1));
|
||||
?>
|
||||
<a href="?room_id=<?= $room['id'] ?>" class="flex items-center p-4 border-b border-border-light dark:border-border-dark <?= ($current_room && $current_room['id'] == $room['id']) ? 'bg-primary-light dark:bg-input-bg-dark' : 'hover:bg-gray-50 dark:hover:bg-gray-800' ?>">
|
||||
<div class="w-12 h-12 rounded-full mr-4 bg-primary text-white flex items-center justify-center font-bold text-xl">
|
||||
<?= htmlspecialchars($avatar_text) ?>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-text-light dark:text-text-dark"><?= htmlspecialchars($room_name) ?></h3>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<!-- Users Tab -->
|
||||
<div x-show="tab === 'users'" class="tab-content">
|
||||
<?php foreach ($users as $user):
|
||||
?>
|
||||
<a href="#" @click.prevent="startPrivateChat(<?= $user['id'] ?>)" class="flex items-center p-4 border-b border-border-light dark:border-border-dark hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($user['username']) ?>" alt="User Avatar" class="w-12 h-12 rounded-full mr-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-text-light dark:text-text-dark"><?= htmlspecialchars($user['username']) ?></h3>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="tab === 'rooms'" class="p-4 border-t border-border-light dark:border-border-dark">
|
||||
<form id="create-room-form">
|
||||
<input type="text" name="room_name" placeholder="Create new room..." class="w-full px-4 py-2 rounded-lg bg-input-bg-light dark:bg-input-bg-dark border focus:ring-2 focus:ring-primary outline-none" required>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-t border-border-light dark:border-border-dark flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($current_username) ?>" alt="My Avatar" class="w-10 h-10 rounded-full mr-3">
|
||||
<span class="font-bold"><?= htmlspecialchars($current_username) ?></span>
|
||||
</div>
|
||||
<a href="logout.php" class="p-2 text-gray-500 rounded-lg dark:text-gray-400 hover:bg-primary-light dark:hover:bg-input-bg-dark hover:text-primary dark:hover:text-white" title="Logout"><span class="material-symbols-outlined">logout</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Chat Area -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<?php if ($current_room):
|
||||
$header_name = $current_room['is_private'] ? $current_room['private_chat_partner'] : $current_room['name'];
|
||||
?>
|
||||
<div class="flex items-center justify-between p-4 border-b bg-white dark:bg-background-dark border-border-light dark:border-border-dark">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold"><?= htmlspecialchars($header_name) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="messages-container" class="flex-1 p-6 overflow-y-auto space-y-6">
|
||||
<div id="no-messages" class="text-center text-gray-500">Loading messages...</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-white dark:bg-background-dark border-t border-border-light dark:border-border-dark">
|
||||
<form id="send-message-form" class="relative" enctype="multipart/form-data">
|
||||
<input type="hidden" name="room_id" value="<?= $current_room['id'] ?>">
|
||||
<input type="text" name="message" placeholder="Type a message..." class="w-full pr-24 pl-12 py-3 rounded-full bg-input-bg-light dark:bg-input-bg-dark border focus:ring-2 focus:ring-primary outline-none" autocomplete="off">
|
||||
<div class="absolute left-4 top-1/2 -translate-y-1/2 flex items-center space-x-3">
|
||||
<label for="attachment-input" class="cursor-pointer text-gray-500 dark:text-placeholder-dark hover:text-primary">
|
||||
<span class="material-symbols-outlined">attach_file</span>
|
||||
</label>
|
||||
<input type="file" name="attachment" id="attachment-input" class="hidden">
|
||||
</div>
|
||||
<button type="submit" class="absolute right-3 top-1/2 -translate-y-1/2 bg-primary text-white rounded-full p-2 hover:bg-accent-blue-hover"><span class="material-symbols-outlined">send</span></button>
|
||||
</form>
|
||||
<div id="attachment-preview" class="mt-2"></div>
|
||||
</div>
|
||||
<?php else:
|
||||
?>
|
||||
<div class="flex-1 flex items-center justify-center text-gray-500">
|
||||
<p>Create or select a room to start chatting.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let lastMessageId = 0;
|
||||
const currentRoomId = <?= $current_room_id ?? 'null' ?>;
|
||||
const currentUsername = "<?= htmlspecialchars($current_username) ?>";
|
||||
|
||||
function themeSwitcher() {
|
||||
return {
|
||||
open: false,
|
||||
theme: 'system',
|
||||
init() {
|
||||
this.theme = localStorage.getItem('theme') || 'system';
|
||||
this.applyTheme();
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (this.theme === 'system') this.applySystemTheme();
|
||||
});
|
||||
},
|
||||
applyTheme() {
|
||||
if (this.theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (this.theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
this.applySystemTheme();
|
||||
}
|
||||
},
|
||||
applySystemTheme() {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
},
|
||||
setTheme(newTheme) {
|
||||
this.theme = newTheme;
|
||||
localStorage.setItem('theme', newTheme);
|
||||
this.applyTheme();
|
||||
this.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
if(messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
scrollToBottom();
|
||||
|
||||
async function startPrivateChat(userId) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'start_private_chat');
|
||||
formData.append('user_id', userId);
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.location.href = `index.php?room_id=${result.room_id}`;
|
||||
} else {
|
||||
alert('Error starting chat: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start private chat:', error);
|
||||
alert('An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessage(msg) {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const noMessagesEl = document.getElementById('no-messages');
|
||||
if (noMessagesEl) noMessagesEl.style.display = 'none';
|
||||
|
||||
const isSender = msg.username === currentUsername;
|
||||
let attachmentHtml = '';
|
||||
if (msg.file_name) {
|
||||
attachmentHtml = `
|
||||
<div class="mt-2">
|
||||
<a href="${msg.file_path}" target="_blank" class="text-blue-500 hover:underline">${escapeHTML(msg.file_name)}</a>
|
||||
<span class="text-xs text-gray-500">(${(msg.file_size / 1024 / 1024).toFixed(2)} MB)</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const messageHtml = `
|
||||
<div class="flex items-start gap-3 ${isSender ? 'justify-end' : ''}">
|
||||
${!isSender ? `<img src="https://i.pravatar.cc/40?u=${escapeHTML(msg.username)}" alt="Avatar" class="w-10 h-10 rounded-full">` : ''}
|
||||
<div class="${isSender ? 'bg-primary text-white' : 'bg-white dark:bg-input-bg-dark'} p-3 rounded-lg max-w-lg shadow">
|
||||
<p class="text-sm font-bold mb-1">${escapeHTML(msg.username)}</p>
|
||||
<p class="text-sm">${escapeHTML(msg.message).replace(/\n/g, '<br>')}</p>
|
||||
${attachmentHtml}
|
||||
<span class="text-xs ${isSender ? 'text-blue-200' : 'text-gray-400'} mt-1 block text-right">${new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
${isSender ? `<img src="https://i.pravatar.cc/40?u=${escapeHTML(currentUsername)}" alt="My Avatar" class="w-10 h-10 rounded-full">` : ''}
|
||||
</div>
|
||||
`;
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHtml);
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
if (str === null || str === undefined) {
|
||||
return '';
|
||||
}
|
||||
var p = document.createElement("p");
|
||||
p.appendChild(document.createTextNode(str));
|
||||
return p.innerHTML;
|
||||
}
|
||||
|
||||
async function pollNewMessages() {
|
||||
if (!currentRoomId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`api.php?action=get_new_messages&room_id=${currentRoomId}&last_message_id=${lastMessageId}`);
|
||||
if (response.ok) {
|
||||
const messages = await response.json();
|
||||
if (messages.length > 0) {
|
||||
const noMessagesEl = document.getElementById('no-messages');
|
||||
if(noMessagesEl && noMessagesEl.innerText === 'Loading messages...') {
|
||||
noMessagesEl.style.display = 'none';
|
||||
}
|
||||
messages.forEach(renderMessage);
|
||||
lastMessageId = messages[messages.length - 1].id;
|
||||
scrollToBottom();
|
||||
} else {
|
||||
const noMessagesEl = document.getElementById('no-messages');
|
||||
if(noMessagesEl && noMessagesEl.innerText === 'Loading messages...') {
|
||||
noMessagesEl.innerText = 'No messages yet. Start the conversation!';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
}
|
||||
|
||||
// Continue polling for real-time updates
|
||||
setTimeout(pollNewMessages, 1000);
|
||||
}
|
||||
|
||||
async function fetchInitialMessages() {
|
||||
if (!currentRoomId) return;
|
||||
|
||||
const response = await fetch(`api.php?action=get_new_messages&room_id=${currentRoomId}&last_message_id=0`);
|
||||
if(response.ok) {
|
||||
const messages = await response.json();
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const noMessagesEl = document.getElementById('no-messages');
|
||||
|
||||
if (messages.length > 0) {
|
||||
noMessagesEl.style.display = 'none';
|
||||
messages.forEach(renderMessage);
|
||||
lastMessageId = messages[messages.length - 1].id;
|
||||
scrollToBottom();
|
||||
} else {
|
||||
noMessagesEl.innerText = 'No messages yet. Start the conversation!';
|
||||
}
|
||||
}
|
||||
// Start long polling after initial fetch
|
||||
setTimeout(pollNewMessages, 1000);
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sendMessageForm = document.getElementById('send-message-form');
|
||||
const attachmentInput = document.getElementById('attachment-input');
|
||||
const attachmentPreview = document.getElementById('attachment-preview');
|
||||
|
||||
if(attachmentInput) {
|
||||
attachmentInput.addEventListener('change', () => {
|
||||
const file = attachmentInput.files[0];
|
||||
if (file) {
|
||||
attachmentPreview.innerHTML = `
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span>${file.name}</span>
|
||||
<button type="button" id="remove-attachment" class="text-red-500">×</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('remove-attachment').addEventListener('click', () => {
|
||||
attachmentInput.value = '';
|
||||
attachmentPreview.innerHTML = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(sendMessageForm) {
|
||||
sendMessageForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(sendMessageForm);
|
||||
|
||||
// Clear form fields immediately for better UX
|
||||
sendMessageForm.querySelector('input[name="message"]').value = '';
|
||||
if(attachmentInput) attachmentInput.value = '';
|
||||
if(attachmentPreview) attachmentPreview.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php?action=send_message', { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert('Error sending message: ' + result.error);
|
||||
}
|
||||
// Don't manually render, let the poller pick it up
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
alert('An unexpected error occurred.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createRoomForm = document.getElementById('create-room-form');
|
||||
if(createRoomForm) {
|
||||
createRoomForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(createRoomForm);
|
||||
formData.append('action', 'create_room');
|
||||
|
||||
try {
|
||||
const response = await fetch('api.php', { method: 'POST', body: formData });
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.location.href = `index.php?room_id=${result.room_id}`;
|
||||
} else {
|
||||
alert('Error creating room: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create room:', error);
|
||||
alert('An unexpected error occurred.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentRoomId) {
|
||||
fetchInitialMessages();
|
||||
} else {
|
||||
const noMessagesEl = document.getElementById('no-messages');
|
||||
if(noMessagesEl) noMessagesEl.innerText = 'Create or select a room to start chatting.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
80
login.php
Normal file
80
login.php
Normal file
@ -0,0 +1,80 @@
|
||||
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Please fill in all fields.';
|
||||
} else {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Invalid username or password.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "Database error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html class="dark" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Login - AD Messaging App</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background-dark text-text-dark flex items-center justify-center h-screen">
|
||||
<div class="w-full max-w-md p-8 space-y-8 bg-input-bg-dark rounded-xl shadow-lg">
|
||||
<h2 class="text-2xl font-bold text-center">Log in to your account</h2>
|
||||
<?php if ($error): ?>
|
||||
<div class="p-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="login.php" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="text-sm font-medium text-gray-300">Username</label>
|
||||
<input id="username" name="username" type="text" required
|
||||
class="w-full px-4 py-2 mt-2 text-white bg-gray-700 border border-gray-600 rounded-lg focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="text-sm font-medium text-gray-300">Password</label>
|
||||
<input id="password" name="password" type="password" required
|
||||
class="w-full px-4 py-2 mt-2 text-white bg-gray-700 border border-gray-600 rounded-lg focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2 font-semibold text-white bg-primary rounded-lg hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary dark:focus:ring-offset-background-dark">
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center text-gray-400">
|
||||
Don't have an account? <a href="register.php" class="font-medium text-primary hover:underline">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
logout.php
Normal file
6
logout.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
22
message_template.php
Normal file
22
message_template.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
$is_sender = $msg['username'] === $current_username;
|
||||
?>
|
||||
<div class="flex items-start gap-3 <?= $is_sender ? 'justify-end' : '' ?>">
|
||||
<?php if (!$is_sender): ?><img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($msg['username']) ?>" alt="Avatar" class="w-10 h-10 rounded-full"><?php endif; ?>
|
||||
<div class="<?= $is_sender ? 'bg-primary text-white' : 'bg-white dark:bg-input-bg-dark' ?> p-3 rounded-lg max-w-lg shadow">
|
||||
<p class="text-sm font-bold mb-1"><?= htmlspecialchars($msg['username']) ?></p>
|
||||
<?php if (!empty($msg['message'])): ?>
|
||||
<p class="text-sm"><?= nl2br(htmlspecialchars($msg['message'])) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($msg['has_attachment']): ?>
|
||||
<div class="mt-2">
|
||||
<a href="<?= htmlspecialchars($msg['file_path']) ?>" target="_blank" class="text-blue-500 hover:underline">
|
||||
<?= htmlspecialchars($msg['file_name']) ?>
|
||||
</a>
|
||||
<span class="text-xs text-gray-500">(${(round($msg['file_size'] / 1024 / 1024, 2))}) MB)</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<span class="text-xs <?= $is_sender ? 'text-blue-200' : 'text-gray-400' ?> mt-1 block text-right"><?= date('h:i A', strtotime($msg['created_at'])) ?></span>
|
||||
</div>
|
||||
<?php if ($is_sender): ?><img src="https://i.pravatar.cc/40?u=<?= htmlspecialchars($current_username) ?>" alt="My Avatar" class="w-10 h-10 rounded-full"><?php endif; ?>
|
||||
</div>
|
||||
81
register.php
Normal file
81
register.php
Normal file
@ -0,0 +1,81 @@
|
||||
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Please fill in all fields.';
|
||||
} else {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
if ($stmt->fetch()) {
|
||||
$error = 'Username already taken.';
|
||||
} else {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
||||
$stmt->execute([$username, $hashed_password]);
|
||||
$_SESSION['user_id'] = $pdo->lastInsertId();
|
||||
$_SESSION['username'] = $username;
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "Database error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html class="dark" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Register - AD Messaging App</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background-dark text-text-dark flex items-center justify-center h-screen">
|
||||
<div class="w-full max-w-md p-8 space-y-8 bg-input-bg-dark rounded-xl shadow-lg">
|
||||
<h2 class="text-2xl font-bold text-center">Create your account</h2>
|
||||
<?php if ($error): ?>
|
||||
<div class="p-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="register.php" class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="text-sm font-medium text-gray-300">Username</label>
|
||||
<input id="username" name="username" type="text" required
|
||||
class="w-full px-4 py-2 mt-2 text-white bg-gray-700 border border-gray-600 rounded-lg focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="text-sm font-medium text-gray-300">Password</label>
|
||||
<input id="password" name="password" type="password" required
|
||||
class="w-full px-4 py-2 mt-2 text-white bg-gray-700 border border-gray-600 rounded-lg focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2 font-semibold text-white bg-primary rounded-lg hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary dark:focus:ring-offset-background-dark">
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center text-gray-400">
|
||||
Already have an account? <a href="login.php" class="font-medium text-primary hover:underline">Log in</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user