This commit is contained in:
Flatlogic Bot 2026-02-15 10:33:41 +00:00
parent 4883125cda
commit ef520f4259
7 changed files with 337 additions and 11 deletions

31
api_v1_channels.php Normal file
View File

@ -0,0 +1,31 @@
<?php
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$server_id = $_POST['server_id'] ?? 0;
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? 'text';
$user_id = $_SESSION['user_id'];
// Check if user is owner of the server or has permissions (simplified check for now: user must be a member)
$stmt = db()->prepare("SELECT 1 FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$server_id, $user_id]);
if ($stmt->fetch() && $name) {
try {
// Basic sanitization for channel name
$name = strtolower(preg_replace('/[^a-zA-Z0-3\-]/', '-', $name));
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, ?, ?)");
$stmt->execute([$server_id, $name, $type]);
$channel_id = db()->lastInsertId();
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
} catch (Exception $e) {
die("Error creating channel: " . $e->getMessage());
}
}
}
header('Location: index.php');

55
api_v1_servers.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'create';
$user_id = $_SESSION['user_id'];
if ($action === 'join') {
$invite_code = $_POST['invite_code'] ?? '';
$stmt = db()->prepare("SELECT id FROM servers WHERE invite_code = ?");
$stmt->execute([$invite_code]);
$server = $stmt->fetch();
if ($server) {
$stmt = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$server['id'], $user_id]);
header('Location: index.php?server_id=' . $server['id']);
exit;
} else {
die("Invalid invite code.");
}
}
$name = $_POST['name'] ?? '';
if ($name) {
try {
$db = db();
$db->beginTransaction();
// Create server
$invite_code = substr(strtoupper(md5(uniqid())), 0, 8);
$stmt = $db->prepare("INSERT INTO servers (name, owner_id, invite_code) VALUES (?, ?, ?)");
$stmt->execute([$name, $user_id, $invite_code]);
$server_id = $db->lastInsertId();
// Add owner as member
$stmt = $db->prepare("INSERT INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$server_id, $user_id]);
// Create default channel
$stmt = $db->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, 'general', 'text')");
$stmt->execute([$server_id]);
$db->commit();
header('Location: index.php?server_id=' . $server_id);
exit;
} catch (Exception $e) {
$db->rollBack();
die("Error creating server: " . $e->getMessage());
}
}
}
header('Location: index.php');

View File

@ -120,6 +120,15 @@ body {
font-size: 0.9em;
}
.server-icon.add-btn {
color: #23a559;
}
.server-icon.add-btn:hover {
background-color: #23a559;
color: white;
}
.channel-category {
color: var(--text-muted);
font-size: 0.75em;
@ -127,6 +136,46 @@ body {
font-weight: bold;
margin-bottom: 8px;
padding-left: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.add-channel-btn {
cursor: pointer;
font-size: 1.2em;
margin-right: 8px;
line-height: 1;
}
.add-channel-btn:hover {
color: var(--text-primary);
}
/* Modals */
.modal-content {
background-color: var(--bg-channels);
color: var(--text-primary);
}
.modal-header {
border-bottom: 1px solid var(--bg-servers);
}
.modal-footer {
border-top: 1px solid var(--bg-servers);
}
.form-control {
background-color: var(--bg-servers);
border: none;
color: var(--text-primary);
}
.form-control:focus {
background-color: var(--bg-servers);
color: var(--text-primary);
box-shadow: 0 0 0 0.25rem rgba(88, 101, 242, 0.25);
}
/* User Panel */

View File

@ -43,6 +43,32 @@ document.addEventListener('DOMContentLoaded', () => {
content: content,
channel_id: channel_id
})
});
const result = await response.json();
if (result.success) {
// If WS is connected, we might want to let WS handle the UI update
// But for simplicity, we append here and also send to WS
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
data: JSON.stringify({
...result.message,
channel_id: channel_id
})
}));
} else {
appendMessage(result.message);
messagesList.scrollTop = messagesList.scrollHeight;
}
} else {
alert('Error: ' + result.error);
}
} catch (err) {
console.error('Failed to send message:', err);
}
});
// Voice
const voiceHandler = new VoiceChannel(ws);
document.querySelectorAll('.voice-item').forEach(item => {
@ -57,6 +83,28 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
function appendMessage(msg) {
const messagesList = document.getElementById('messages-list');
const div = document.createElement('div');
div.className = 'message-item';
div.innerHTML = `
<div class="message-avatar"></div>
<div class="message-content">
<div class="message-author">
${msg.username}
<span class="message-time">${msg.time}</span>
</div>
<div class="message-text">
${msg.content.replace(/\n/g, '<br>')}
</div>
</div>
`;
messagesList.appendChild(div);
}
});
});
const result = await response.json();
if (result.success) {

View File

@ -12,7 +12,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$stmt = db()->prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)");
$stmt->execute([$username, $email, $hash]);
$_SESSION['user_id'] = db()->lastInsertId();
$userId = db()->lastInsertId();
// Add to default server
$stmt = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (1, ?)");
$stmt->execute([$userId]);
$_SESSION['user_id'] = $userId;
header('Location: ../index.php');
exit;
} catch (Exception $e) {

View File

@ -67,6 +67,15 @@ CREATE TABLE IF NOT EXISTS user_roles (
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS server_members (
server_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (server_id, user_id),
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Seed initial data
INSERT IGNORE INTO users (id, username, email, password_hash, status) VALUES
(1, 'System', 'system@local', '$2y$10$xyz', 'online');
@ -75,6 +84,8 @@ INSERT IGNORE INTO servers (id, name, owner_id, invite_code) VALUES
(1, 'General Community', 1, 'GEN-123'),
(2, 'Flatlogic Devs', 1, 'DEV-456');
INSERT IGNORE INTO server_members (server_id, user_id) VALUES (1, 1), (2, 1);
INSERT IGNORE INTO channels (id, server_id, name, type) VALUES
(1, 1, 'general', 'text'),
(2, 1, 'random', 'text'),

146
index.php
View File

@ -5,10 +5,20 @@ requireLogin();
$user = getCurrentUser();
$current_user_id = $user['id'];
// Fetch servers
$servers = db()->query("SELECT * FROM servers LIMIT 10")->fetchAll();
// Fetch servers user is member of
$stmt = db()->prepare("
SELECT s.* FROM servers s
JOIN server_members sm ON s.id = sm.server_id
WHERE sm.user_id = ?
LIMIT 20
");
$stmt->execute([$current_user_id]);
$servers = $stmt->fetchAll();
$active_server_id = $_GET['server_id'] ?? ($servers[0]['id'] ?? 1);
// If no servers found, we might want to show a default or an empty state
// For now, let's assume the seed data or the first server works
// Fetch channels
$stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ?");
$stmt->execute([$active_server_id]);
@ -30,6 +40,16 @@ $messages = $stmt->fetchAll();
$current_channel_name = 'general';
foreach($channels as $c) if($c['id'] == $active_channel_id) $current_channel_name = $c['name'];
// Fetch members
$stmt = db()->prepare("
SELECT u.username, u.avatar_url, u.status
FROM users u
JOIN server_members sm ON u.id = sm.user_id
WHERE sm.server_id = ?
");
$stmt->execute([$active_server_id]);
$members = $stmt->fetchAll();
// SEO & Env tags
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Discord-like messaging app built with PHP';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
@ -69,15 +89,25 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php echo mb_substr($s['name'], 0, 1); ?>
</a>
<?php endforeach; ?>
<a href="#" class="server-icon add-btn" title="Add a Server" data-bs-toggle="modal" data-bs-target="#addServerModal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</a>
</div>
<!-- Channels Sidebar -->
<div class="channels-sidebar">
<div class="channels-header">
<?php echo htmlspecialchars($servers[0]['name'] ?? 'Server'); ?>
<?php
$active_server_name = 'Server';
foreach($servers as $s) if($s['id'] == $active_server_id) $active_server_name = $s['name'];
echo htmlspecialchars($active_server_name);
?>
</div>
<div class="channels-list">
<div class="channel-category">Text Channels</div>
<div class="channel-category">
<span>Text Channels</span>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="text">+</span>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'text') continue; ?>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item <?php echo $c['id'] == $active_channel_id ? 'active' : ''; ?>">
@ -85,7 +115,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</a>
<?php endforeach; ?>
<div class="channel-category" style="margin-top: 16px;">Voice Channels</div>
<div class="channel-category" style="margin-top: 16px;">
<span>Voice Channels</span>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="voice">+</span>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'voice') continue; ?>
<div class="channel-item voice-item" data-channel-id="<?php echo $c['id']; ?>">
<?php echo htmlspecialchars($c['name']); ?>
@ -150,17 +183,110 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Members Sidebar -->
<div class="members-sidebar">
<div style="color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; font-weight: bold; margin-bottom: 8px;">
Online 1
<div style="color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; font-weight: bold; margin-bottom: 16px;">
Members <?php echo count($members); ?>
</div>
<div class="channel-item" style="color: var(--text-primary);">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: #23a559;"></div>
System
<?php foreach($members as $m): ?>
<div class="channel-item" style="color: var(--text-primary); margin-bottom: 8px;">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: <?php echo $m['status'] == 'online' ? '#23a559' : '#80848e'; ?>; position: relative;">
<?php if($m['status'] == 'online'): ?>
<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>
<?php endif; ?>
</div>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<?php echo htmlspecialchars($m['username']); ?>
</span>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Add Server Modal -->
<div class="modal fade" id="addServerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create or Join a server</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs nav-fill" id="serverTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active text-white border-0 bg-transparent" id="create-tab" data-bs-toggle="tab" data-bs-target="#create-pane" type="button">Create</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="join-tab" data-bs-toggle="tab" data-bs-target="#join-pane" type="button">Join</button>
</li>
</ul>
<div class="tab-content p-3">
<div class="tab-pane fade show active" id="create-pane">
<form action="api_v1_servers.php" method="POST">
<p style="color: var(--text-muted); font-size: 0.9em;">Give your new server a personality with a name.</p>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Server Name</label>
<input type="text" name="name" class="form-control" placeholder="My Awesome Server" required>
</div>
<button type="submit" class="btn btn-primary w-100" style="background-color: var(--blurple); border: none;">Create Server</button>
</form>
</div>
<div class="tab-pane fade" id="join-pane">
<form action="api_v1_servers.php" method="POST">
<input type="hidden" name="action" value="join">
<p style="color: var(--text-muted); font-size: 0.9em;">Enter an invite code to join an existing server.</p>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Invite Code</label>
<input type="text" name="invite_code" class="form-control" placeholder="GEN-123" required>
</div>
<button type="submit" class="btn btn-success w-100" style="background-color: #23a559; border: none;">Join Server</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Channel Modal -->
<div class="modal fade" id="addChannelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create Channel</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="type" id="channel-type-input" value="text">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<div class="input-group">
<span class="input-group-text bg-dark border-0 text-muted" id="channel-hash-prefix">#</span>
<input type="text" name="name" class="form-control" placeholder="new-channel" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 24px;">Create Channel</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
// Handle channel type in modal
document.querySelectorAll('.add-channel-btn').forEach(btn => {
btn.addEventListener('click', function() {
const type = this.getAttribute('data-type');
document.getElementById('channel-type-input').value = type;
document.getElementById('channel-hash-prefix').textContent = type === 'text' ? '#' : '🔊';
});
});
</script>
</body>
</html>