This commit is contained in:
Flatlogic Bot 2025-11-17 01:22:13 +00:00
parent 3db6af498b
commit 2516f4214e
9 changed files with 417 additions and 23 deletions

159
api.php Normal file
View File

@ -0,0 +1,159 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'User not authenticated']);
exit;
}
$action = $_GET['action'] ?? '';
switch ($action) {
case 'search_users':
search_users();
break;
case 'start_conversation':
start_conversation();
break;
case 'get_conversations':
get_conversations();
break;
case 'get_messages':
get_messages();
break;
case 'send_message':
send_message();
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
exit;
}
function search_users() {
$term = $_GET['term'] ?? '';
if (empty($term)) {
echo json_encode([]);
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE username LIKE ? AND id != ?");
$stmt->execute(['%' . $term . '%', $_SESSION['user_id']]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/json');
echo json_encode($users);
}
function start_conversation() {
$recipient_id = $_POST['recipient_id'] ?? '';
if (empty($recipient_id)) {
http_response_code(400);
echo json_encode(['error' => 'Recipient ID is required']);
exit;
}
$user_id = $_SESSION['user_id'];
$pdo = db();
// Check if a conversation already exists between the two users
$stmt = $pdo->prepare("
SELECT c.id
FROM conversations c
JOIN conversation_participants cp1 ON c.id = cp1.conversation_id
JOIN conversation_participants cp2 ON c.id = cp2.conversation_id
WHERE cp1.user_id = ? AND cp2.user_id = ?
");
$stmt->execute([$user_id, $recipient_id]);
$conversation = $stmt->fetch(PDO::FETCH_ASSOC);
if ($conversation) {
// Conversation already exists
header('Content-Type: application/json');
echo json_encode(['conversation_id' => $conversation['id']]);
exit;
}
// Create a new conversation
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT INTO conversations () VALUES ()");
$stmt->execute();
$conversation_id = $pdo->lastInsertId();
$stmt = $pdo->prepare("INSERT INTO conversation_participants (conversation_id, user_id) VALUES (?, ?), (?, ?)");
$stmt->execute([$conversation_id, $user_id, $conversation_id, $recipient_id]);
$pdo->commit();
header('Content-Type: application/json');
echo json_encode(['conversation_id' => $conversation_id]);
} catch (Exception $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['error' => 'Failed to create conversation']);
}
}
function get_conversations() {
$user_id = $_SESSION['user_id'];
$pdo = db();
$stmt = $pdo->prepare("
SELECT c.id, u.username, u.id as user_id
FROM conversations c
JOIN conversation_participants cp ON c.id = cp.conversation_id
JOIN users u ON u.id = cp.user_id
WHERE c.id IN (
SELECT conversation_id
FROM conversation_participants
WHERE user_id = ?
) AND cp.user_id != ?
");
$stmt->execute([$user_id, $user_id]);
$conversations = $stmt->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/json');
echo json_encode($conversations);
}
function get_messages() {
$conversation_id = $_GET['conversation_id'] ?? '';
if (empty($conversation_id)) {
http_response_code(400);
echo json_encode(['error' => 'Conversation ID is required']);
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC");
$stmt->execute([$conversation_id]);
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/json');
echo json_encode($messages);
}
function send_message() {
$conversation_id = $_POST['conversation_id'] ?? '';
$message_text = $_POST['message_text'] ?? '';
if (empty($conversation_id) || empty($message_text)) {
http_response_code(400);
echo json_encode(['error' => 'Conversation ID and message text are required']);
exit;
}
$sender_id = $_SESSION['user_id'];
$pdo = db();
$stmt = $pdo->prepare("INSERT INTO messages (conversation_id, sender_id, message_text) VALUES (?, ?, ?)");
$stmt->execute([$conversation_id, $sender_id, $message_text]);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
}

View File

@ -158,3 +158,21 @@ body {
background-color: #ffffff; background-color: #ffffff;
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
} }
.avatar-placeholder {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #6c757d;
}
.avatar-placeholder.avatar-sm {
width: 40px;
height: 40px;
font-size: 20px;
}

View File

@ -1,29 +1,191 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const chatForm = document.getElementById('chat-form'); const chatForm = document.getElementById('chat-form');
const messageInput = document.getElementById('message-input'); const messageInput = document.getElementById('message-input');
const userSearchForm = document.getElementById('user-search-form');
const userSearchInput = document.getElementById('user-search-input');
const searchResultsContainer = document.getElementById('search-results');
const conversationListContainer = document.getElementById('conversation-list-container');
const chatBody = document.querySelector('.chat-body');
const chatHeaderName = document.querySelector('.chat-header .name');
const chatHeaderStatus = document.querySelector('.chat-header .status');
let currentConversationId = null;
if (chatForm) { if (chatForm) {
chatForm.addEventListener('submit', function(e) { chatForm.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
const message = messageInput.value.trim(); const message = messageInput.value.trim();
if (message) { if (message && currentConversationId) {
console.log('Sending message:', message); sendMessage(currentConversationId, message);
// In a real app, you'd send this to the server. }
// For this demo, we'll just clear the input. });
messageInput.value = ''; }
// Optional: Add the message to the UI for instant feedback if (userSearchForm) {
const chatBody = document.querySelector('.chat-body'); userSearchForm.addEventListener('submit', function(e) {
e.preventDefault();
const searchTerm = userSearchInput.value.trim();
if (searchTerm) {
searchUsers(searchTerm);
}
});
}
function searchUsers(term) {
fetch(`api.php?action=search_users&term=${term}`)
.then(response => response.json())
.then(users => {
displaySearchResults(users);
})
.catch(error => console.error('Error searching users:', error));
}
function displaySearchResults(users) {
searchResultsContainer.innerHTML = '';
if (users.length === 0) {
searchResultsContainer.innerHTML = '<p class="text-center text-muted p-4">No users found.</p>';
return;
}
users.forEach(user => {
const userElement = document.createElement('div');
userElement.classList.add('conversation-item');
userElement.dataset.userId = user.id;
userElement.innerHTML = `
<div class="avatar-placeholder me-3">
<i class="bi bi-person-fill"></i>
</div>
<div class="conversation-info">
<h6 class="name mb-0">${user.username}</h6>
</div>
`;
userElement.addEventListener('click', () => {
startConversation(user.id);
});
searchResultsContainer.appendChild(userElement);
});
}
function startConversation(recipientId) {
const formData = new FormData();
formData.append('recipient_id', recipientId);
fetch('api.php?action=start_conversation', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.conversation_id) {
loadMessages(data.conversation_id);
userSearchInput.value = '';
searchResultsContainer.innerHTML = '';
loadConversations();
} else {
console.error('Failed to start conversation:', data.error);
}
})
.catch(error => console.error('Error starting conversation:', error));
}
function loadConversations() {
fetch('api.php?action=get_conversations')
.then(response => response.json())
.then(conversations => {
displayConversations(conversations);
})
.catch(error => console.error('Error loading conversations:', error));
}
function displayConversations(conversations) {
conversationListContainer.innerHTML = '';
if (conversations.length === 0) {
conversationListContainer.innerHTML = '<p class="text-center text-muted p-4">No conversations yet.</p>';
return;
}
conversations.forEach(conversation => {
const conversationElement = document.createElement('div');
conversationElement.classList.add('conversation-item');
conversationElement.dataset.conversationId = conversation.id;
conversationElement.dataset.recipientId = conversation.user_id;
conversationElement.innerHTML = `
<div class="avatar-placeholder me-3">
<i class="bi bi-person-fill"></i>
</div>
<div class="conversation-info">
<h6 class="name mb-0">${conversation.username}</h6>
</div>
`;
conversationElement.addEventListener('click', () => {
loadMessages(conversation.id, conversation.username);
});
conversationListContainer.appendChild(conversationElement);
});
}
function loadMessages(conversationId, username) {
currentConversationId = conversationId;
chatHeaderName.textContent = username;
chatHeaderStatus.textContent = 'Online'; // Placeholder
fetch(`api.php?action=get_messages&conversation_id=${conversationId}`)
.then(response => response.json())
.then(messages => {
displayMessages(messages);
})
.catch(error => console.error('Error loading messages:', error));
}
function displayMessages(messages) {
chatBody.innerHTML = '';
if (messages.length === 0) {
chatBody.innerHTML = '<div class="text-center text-muted" style="margin-top: auto; margin-bottom: auto;"><p>No messages yet. Say hi!</p></div>';
return;
}
messages.forEach(message => {
const messageElement = document.createElement('div');
const isSent = message.sender_id == window.userId;
messageElement.classList.add('message', isSent ? 'sent' : 'received');
messageElement.innerHTML = `
<div class="message-bubble">${message.message_text}</div>
<div class="message-time">${new Date(message.created_at).toLocaleTimeString()}</div>
`;
chatBody.appendChild(messageElement);
});
chatBody.scrollTop = chatBody.scrollHeight;
}
function sendMessage(conversationId, messageText) {
const formData = new FormData();
formData.append('conversation_id', conversationId);
formData.append('message_text', messageText);
fetch('api.php?action=send_message', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
messageInput.value = '';
// Add message to UI immediately
const messageElement = document.createElement('div'); const messageElement = document.createElement('div');
messageElement.classList.add('message', 'sent'); messageElement.classList.add('message', 'sent');
messageElement.innerHTML = ` messageElement.innerHTML = `
<div class="message-bubble">${message}</div> <div class="message-bubble">${messageText}</div>
<div class="message-time">Just now</div> <div class="message-time">Just now</div>
`; `;
chatBody.appendChild(messageElement); chatBody.appendChild(messageElement);
chatBody.scrollTop = chatBody.scrollHeight; chatBody.scrollTop = chatBody.scrollHeight;
} else {
console.error('Failed to send message:', data.error);
} }
}); })
.catch(error => console.error('Error sending message:', error));
} }
// Load conversations on page load
loadConversations();
}); });

View File

@ -4,18 +4,35 @@ require_once __DIR__ . '/config.php';
function run_migrations() { function run_migrations() {
try { try {
$pdo = db(); $pdo = db();
$migration_file = __DIR__ . '/migrations/001_create_users_table.sql';
if (file_exists($migration_file)) { // Create migrations table if it doesn't exist
$sql = file_get_contents($migration_file); $pdo->exec("CREATE TABLE IF NOT EXISTS migrations (id INT AUTO_INCREMENT PRIMARY KEY, migration_name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
$pdo->exec($sql);
echo "Migration '001_create_users_table.sql' applied successfully.\n"; // Get all migration files
} else { $migration_files = glob(__DIR__ . '/migrations/*.sql');
echo "Migration file not found.\n"; sort($migration_files);
// Get already run migrations
$stmt = $pdo->query("SELECT migration_name FROM migrations");
$run_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($migration_files as $migration_file) {
$migration_name = basename($migration_file);
if (!in_array($migration_name, $run_migrations)) {
$sql = file_get_contents($migration_file);
$pdo->exec($sql);
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
$stmt->execute([$migration_name]);
echo "Migration '" . $migration_name . "' applied successfully.\n";
}
} }
} catch (PDOException $e) { } catch (PDOException $e) {
die("Migration failed: " . $e->getMessage() . "\n"); die("Migration failed: " . $e->getMessage() . "\n");
} }
} }
run_migrations(); run_migrations();

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS conversation_participants (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT NOT NULL,
user_id INT NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES conversations(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT NOT NULL,
sender_id INT NOT NULL,
message_text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id),
FOREIGN KEY (sender_id) REFERENCES users(id)
);

View File

@ -33,6 +33,10 @@ if (!isset($_SESSION['user_id'])) {
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" href="https://flatlogic.com/favicon.ico" type="image/x-icon"> <link rel="icon" href="https://flatlogic.com/favicon.ico" type="image/x-icon">
<script>
window.userId = <?php echo $_SESSION['user_id']; ?>;
</script>
<!-- Styles --> <!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
@ -44,16 +48,23 @@ if (!isset($_SESSION['user_id'])) {
<!-- Sidebar --> <!-- Sidebar -->
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header d-flex align-items-center"> <div class="sidebar-header d-flex align-items-center">
<img src="https://i.pravatar.cc/150?u=me" alt="My Avatar" class="avatar me-3"> <div class="avatar-placeholder me-3">
<i class="bi bi-person-fill"></i>
</div>
<div> <div>
<h5 class="mb-0 fw-bold">You</h5> <h5 class="mb-0 fw-bold">You</h5>
<p class="text-muted mb-0 small">My status message...</p> <p class="text-muted mb-0 small">My status message...</p>
</div> </div>
</div> </div>
<div class="sidebar-search"> <div class="sidebar-search">
<input type="text" class="form-control rounded-pill" placeholder="Search or start new chat"> <form id="user-search-form">
<input type="text" id="user-search-input" class="form-control rounded-pill" placeholder="Search for users...">
</form>
</div> </div>
<div class="conversation-list"> <div id="search-results" class="conversation-list">
<!-- Search results will be injected here -->
</div>
<div class="conversation-list" id="conversation-list-container">
<div class="text-center text-muted p-4"> <div class="text-center text-muted p-4">
<i class="bi bi-chat-dots fs-2"></i> <i class="bi bi-chat-dots fs-2"></i>
<p class="mt-2">No conversations yet.</p> <p class="mt-2">No conversations yet.</p>
@ -66,7 +77,9 @@ if (!isset($_SESSION['user_id'])) {
<!-- Chat Header --> <!-- Chat Header -->
<header class="chat-header"> <header class="chat-header">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/150?u=placeholder" alt="Avatar" class="avatar avatar-sm me-3"> <div class="avatar-placeholder avatar-sm me-3">
<i class="bi bi-person-fill"></i>
</div>
<div> <div>
<h5 class="mb-0 name">Select a Conversation</h5> <h5 class="mb-0 name">Select a Conversation</h5>
<p class="mb-0 status text-muted">Offline</p> <p class="mb-0 status text-muted">Offline</p>