Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
3953d581c3 version one 2025-11-24 05:17:23 +00:00
6 changed files with 840 additions and 141 deletions

208
api.php Normal file
View 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
View File

@ -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">&times;</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
View 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
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header("Location: login.php");
exit;

22
message_template.php Normal file
View 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
View 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>