3
This commit is contained in:
parent
3db6af498b
commit
2516f4214e
159
api.php
Normal file
159
api.php
Normal 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]);
|
||||
}
|
||||
@ -158,3 +158,21 @@ body {
|
||||
background-color: #ffffff;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1,29 +1,191 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
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) {
|
||||
chatForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const message = messageInput.value.trim();
|
||||
if (message) {
|
||||
console.log('Sending message:', message);
|
||||
// In a real app, you'd send this to the server.
|
||||
// For this demo, we'll just clear the input.
|
||||
messageInput.value = '';
|
||||
if (message && currentConversationId) {
|
||||
sendMessage(currentConversationId, message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Optional: Add the message to the UI for instant feedback
|
||||
const chatBody = document.querySelector('.chat-body');
|
||||
if (userSearchForm) {
|
||||
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');
|
||||
messageElement.classList.add('message', 'sent');
|
||||
messageElement.innerHTML = `
|
||||
<div class="message-bubble">${message}</div>
|
||||
<div class="message-bubble">${messageText}</div>
|
||||
<div class="message-time">Just now</div>
|
||||
`;
|
||||
chatBody.appendChild(messageElement);
|
||||
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();
|
||||
});
|
||||
|
||||
@ -4,18 +4,35 @@ require_once __DIR__ . '/config.php';
|
||||
function run_migrations() {
|
||||
try {
|
||||
$pdo = db();
|
||||
$migration_file = __DIR__ . '/migrations/001_create_users_table.sql';
|
||||
if (file_exists($migration_file)) {
|
||||
$sql = file_get_contents($migration_file);
|
||||
$pdo->exec($sql);
|
||||
echo "Migration '001_create_users_table.sql' applied successfully.\n";
|
||||
} else {
|
||||
echo "Migration file not found.\n";
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
$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)");
|
||||
|
||||
// Get all migration files
|
||||
$migration_files = glob(__DIR__ . '/migrations/*.sql');
|
||||
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) {
|
||||
die("Migration failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
run_migrations();
|
||||
|
||||
run_migrations();
|
||||
5
db/migrations/000_create_migrations_table.sql
Normal file
5
db/migrations/000_create_migrations_table.sql
Normal 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
|
||||
);
|
||||
4
db/migrations/002_create_conversations_table.sql
Normal file
4
db/migrations/002_create_conversations_table.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@ -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)
|
||||
);
|
||||
9
db/migrations/004_create_messages_table.sql
Normal file
9
db/migrations/004_create_messages_table.sql
Normal 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)
|
||||
);
|
||||
21
index.php
21
index.php
@ -33,6 +33,10 @@ if (!isset($_SESSION['user_id'])) {
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="https://flatlogic.com/favicon.ico" type="image/x-icon">
|
||||
|
||||
<script>
|
||||
window.userId = <?php echo $_SESSION['user_id']; ?>;
|
||||
</script>
|
||||
|
||||
<!-- 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-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
@ -44,16 +48,23 @@ if (!isset($_SESSION['user_id'])) {
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<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>
|
||||
<h5 class="mb-0 fw-bold">You</h5>
|
||||
<p class="text-muted mb-0 small">My status message...</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 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">
|
||||
<i class="bi bi-chat-dots fs-2"></i>
|
||||
<p class="mt-2">No conversations yet.</p>
|
||||
@ -66,7 +77,9 @@ if (!isset($_SESSION['user_id'])) {
|
||||
<!-- Chat Header -->
|
||||
<header class="chat-header">
|
||||
<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>
|
||||
<h5 class="mb-0 name">Select a Conversation</h5>
|
||||
<p class="mb-0 status text-muted">Offline</p>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user