diff --git a/api/upload_badge_image.php b/api/upload_badge_image.php new file mode 100644 index 0000000..27a3561 --- /dev/null +++ b/api/upload_badge_image.php @@ -0,0 +1,74 @@ + false, 'error' => 'Non autorisé']); + exit; +} + +$server_id = $_POST['server_id'] ?? 0; +if (!$server_id) { + echo json_encode(['success' => false, 'error' => 'ID de serveur manquant']); + exit; +} + +// Permissions check +$can_manage = Permissions::hasPermission($user['id'], $server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($user['id'], $server_id, Permissions::ADMINISTRATOR); +// Also check owner +$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?"); +$stmt->execute([$server_id]); +$server = $stmt->fetch(); +$is_owner = ($server && $server['owner_id'] == $user['id']); + +if (!$is_owner && !$can_manage) { + echo json_encode(['success' => false, 'error' => 'Permissions insuffisantes']); + exit; +} + +if (!isset($_FILES['badge_image']) || $_FILES['badge_image']['error'] !== UPLOAD_ERR_OK) { + echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu']); + exit; +} + +$file = $_FILES['badge_image']; +$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml']; +$maxSize = 1 * 1024 * 1024; // 1MB for badges + +if (!in_array($file['type'], $allowedTypes)) { + echo json_encode(['success' => false, 'error' => 'Format non supporté']); + exit; +} + +if ($file['size'] > $maxSize) { + echo json_encode(['success' => false, 'error' => 'Fichier trop gros (max 1Mo)']); + exit; +} + +$extension = pathinfo($file['name'], PATHINFO_EXTENSION); +if (empty($extension)) { + $extensions = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/webp' => 'webp', + 'image/gif' => 'gif', + 'image/svg+xml' => 'svg' + ]; + $extension = $extensions[$file['type']] ?? 'png'; +} + +$filename = 'badge_' . $server_id . '_' . time() . '_' . rand(1000, 9999) . '.' . $extension; +$dir = __DIR__ . '/../assets/images/badges/'; +if (!is_dir($dir)) mkdir($dir, 0775, true); + +$targetPath = $dir . $filename; +$relativeUrl = 'assets/images/badges/' . $filename; + +if (move_uploaded_file($file['tmp_name'], $targetPath)) { + echo json_encode(['success' => true, 'url' => $relativeUrl]); +} else { + echo json_encode(['success' => false, 'error' => 'Erreur d\'écriture']); +} diff --git a/api_v1_badges.php b/api_v1_badges.php new file mode 100644 index 0000000..a59883b --- /dev/null +++ b/api_v1_badges.php @@ -0,0 +1,105 @@ + false, 'error' => 'Missing server_id']); + exit; + } + + // Verify user is in server + $stmt = db()->prepare("SELECT * FROM server_members WHERE server_id = ? AND user_id = ?"); + $stmt->execute([$server_id, $user_id]); + if (!$stmt->fetch()) { + echo json_encode(['success' => false, 'error' => 'Access denied']); + exit; + } + + $stmt = db()->prepare("SELECT * FROM server_badges WHERE server_id = ? ORDER BY created_at DESC"); + $stmt->execute([$server_id]); + $badges = $stmt->fetchAll(); + + echo json_encode([ + 'success' => true, + 'badges' => $badges + ]); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $data['action'] ?? ''; + $server_id = $data['server_id'] ?? 0; + + // Permissions check + $stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?"); + $stmt->execute([$server_id]); + $server = $stmt->fetch(); + $is_owner = ($server && $server['owner_id'] == $user_id); + $can_manage = Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR); + + if (!$is_owner && !$can_manage) { + echo json_encode(['success' => false, 'error' => 'Unauthorized']); + exit; + } + + if ($action === 'create') { + $name = $data['name'] ?? 'New Badge'; + $image_url = $data['image_url'] ?? ''; + + if (empty($image_url)) { + echo json_encode(['success' => false, 'error' => 'Image requise']); + exit; + } + + $stmt = db()->prepare("INSERT INTO server_badges (server_id, name, image_url) VALUES (?, ?, ?)"); + $stmt->execute([$server_id, $name, $image_url]); + echo json_encode(['success' => true, 'badge_id' => db()->lastInsertId()]); + } elseif ($action === 'update') { + $badge_id = $data['id'] ?? 0; + $name = $data['name'] ?? ''; + $image_url = $data['image_url'] ?? ''; + + $stmt = db()->prepare("UPDATE server_badges SET name = ?, image_url = ? WHERE id = ? AND server_id = ?"); + $stmt->execute([$name, $image_url, $badge_id, $server_id]); + echo json_encode(['success' => true]); + } elseif ($action === 'delete') { + $badge_id = $data['id'] ?? 0; + $stmt = db()->prepare("DELETE FROM server_badges WHERE id = ? AND server_id = ?"); + $stmt->execute([$badge_id, $server_id]); + echo json_encode(['success' => true]); + } elseif ($action === 'set_user_badges') { + $target_user_id = $data['user_id'] ?? 0; + $badge_ids = $data['badge_ids'] ?? []; + + $db = db(); + $db->beginTransaction(); + try { + $stmt = $db->prepare("DELETE FROM member_badges WHERE user_id = ? AND server_id = ?"); + $stmt->execute([$target_user_id, $server_id]); + + if (!empty($badge_ids)) { + $stmt = $db->prepare("INSERT INTO member_badges (server_id, user_id, badge_id) VALUES (?, ?, ?)"); + foreach ($badge_ids as $bid) { + $check = $db->prepare("SELECT id FROM server_badges WHERE id = ? AND server_id = ?"); + $check->execute([$bid, $server_id]); + if ($check->fetch()) { + $stmt->execute([$server_id, $target_user_id, $bid]); + } + } + } + $db->commit(); + echo json_encode(['success' => true]); + } catch (Exception $e) { + $db->rollBack(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + } + exit; +} diff --git a/api_v1_messages.php b/api_v1_messages.php index c2ec195..40d3455 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -48,12 +48,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $query = " SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, - (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon + (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data FROM messages m JOIN users u ON m.user_id = u.id WHERE m.channel_id = ? AND m.is_pinned = 1 "; - $params = [$server_id ?: 0, $server_id ?: 0, $channel_id]; + $params = [$server_id ?: 0, $server_id ?: 0, $server_id ?: 0, $channel_id]; if ($thread_id !== null) { $query .= " AND m.thread_id = ?"; @@ -93,12 +94,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $query = " SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, - (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon + (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data FROM messages m JOIN users u ON m.user_id = u.id WHERE m.channel_id = ? AND m.id > ? "; - $params = [$server_id ?: 0, $server_id ?: 0, $channel_id, $after_id]; + $params = [$server_id ?: 0, $server_id ?: 0, $server_id ?: 0, $channel_id, $after_id]; if ($thread_id !== null) { $query .= " AND m.thread_id = ?"; @@ -293,12 +295,13 @@ try { $stmt = db()->prepare(" SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, - (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon + (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ? "); - $stmt->execute([$server_id ?: 0, $server_id ?: 0, $last_id]); + $stmt->execute([$server_id ?: 0, $server_id ?: 0, $server_id ?: 0, $last_id]); $msg = $stmt->fetch(); echo json_encode([ @@ -310,6 +313,7 @@ try { 'avatar_url' => $msg['avatar_url'], 'role_color' => $msg['role_color'], 'role_icon' => $msg['role_icon'], + 'badge_data' => $msg['badge_data'], 'content' => $msg['content'], 'attachment_url' => $msg['attachment_url'], 'metadata' => $msg['metadata'] ? json_decode($msg['metadata']) : null, diff --git a/api_v1_roles.php b/api_v1_roles.php index a88181c..7ee3c76 100644 --- a/api_v1_roles.php +++ b/api_v1_roles.php @@ -27,12 +27,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $roles = $stmt->fetchAll(); // Fetch members and their roles - $stmt = db()->prepare(" + $stmt = db()->prepare(" SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url, - GROUP_CONCAT(r.id) as role_ids, - GROUP_CONCAT(r.name) as role_names, + GROUP_CONCAT(DISTINCT r.id) as role_ids, + GROUP_CONCAT(DISTINCT r.name) as role_names, (SELECT r2.color FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_color, - (SELECT r2.icon_url FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_icon + (SELECT r2.icon_url FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_icon, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data FROM users u JOIN server_members sm ON u.id = sm.user_id LEFT JOIN user_roles ur ON u.id = ur.user_id @@ -40,7 +41,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { WHERE sm.server_id = ? GROUP BY u.id "); - $stmt->execute([$server_id, $server_id, $server_id, $server_id]); + $stmt->execute([$server_id, $server_id, $server_id, $server_id, $server_id]); $members = $stmt->fetchAll(); $filtered_members = null; diff --git a/assets/images/badges/badge_1_1771598872_8939.jpg b/assets/images/badges/badge_1_1771598872_8939.jpg new file mode 100644 index 0000000..ab5fcf7 Binary files /dev/null and b/assets/images/badges/badge_1_1771598872_8939.jpg differ diff --git a/assets/js/main.js b/assets/js/main.js index 5b7ea1a..e20f0f5 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -936,6 +936,7 @@ document.addEventListener('DOMContentLoaded', () => { const username = memberItem.dataset.username; const avatar = memberItem.dataset.avatar; const roleIds = (memberItem.dataset.roleIds || '').split(',').filter(id => id); + const badgeData = (memberItem.dataset.badgeData || '').split(':::').filter(d => d); // Create or show member menu document.querySelector('.member-context-menu')?.remove(); @@ -950,6 +951,24 @@ document.addEventListener('DOMContentLoaded', () => { menu.style.top = `${rect.top}px`; menu.style.left = `${rect.left - 190}px`; + let badgesHtml = ''; + if (badgeData.length > 0) { + badgesHtml = ` +
+
Badges
+
+ ${badgeData.map(d => { + const parts = d.split('|'); + const name = parts[0]; + const url = parts[1]; + return ``; + }).join('')} +
+
+
+ `; + } + let rolesHtml = ''; if (roleIds.length > 0) { // Deduplicate and filter valid roles from serverRoles @@ -979,9 +998,13 @@ document.addEventListener('DOMContentLoaded', () => { ${escapeHTML(username)}
+ ${badgesHtml} ${rolesHtml} - ${(window.isServerOwner || window.canManageServer) ? `` : ''} + ${(window.isServerOwner || window.canManageServer) ? ` + + + ` : ''} `; document.body.appendChild(menu); @@ -1008,6 +1031,8 @@ document.addEventListener('DOMContentLoaded', () => { } } else if (action === 'edit-roles') { openEditUserRolesModal(userId, username, avatar); + } else if (action === 'edit-badges') { + openEditUserBadgesModal(userId, username, avatar); } menu.remove(); }; @@ -1678,6 +1703,7 @@ document.addEventListener('DOMContentLoaded', () => { item.dataset.username = m.username; item.dataset.avatar = m.avatar_url || ''; item.dataset.roleIds = m.role_ids || ''; + item.dataset.badgeData = m.badge_data || ''; item.style.color = 'var(--text-primary)'; item.style.marginBottom = '8px'; item.style.cursor = 'pointer'; @@ -1797,23 +1823,36 @@ document.addEventListener('DOMContentLoaded', () => { item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-2 rounded bg-dark'; const roleIconHtml = renderRoleIconJS(member.role_icon, '14px'); + let badgesHtml = ''; + if (member.badge_data) { + member.badge_data.split(':::').forEach(d => { + const parts = d.split('|'); + const name = parts[0]; + const url = parts[1]; + badgesHtml += ``; + }); + } item.innerHTML = `
-
+
${escapeHTML(member.username)} ${roleIconHtml} +
${badgesHtml}
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
- ${(window.isServerOwner || window.canManageServer) ? ` - - ` : ''} +
+ ${(window.isServerOwner || window.canManageServer) ? ` + + + ` : ''} +
`; membersList.appendChild(item); }); @@ -1821,9 +1860,13 @@ document.addEventListener('DOMContentLoaded', () => { // Add listener for the button in members list tab membersList?.addEventListener('click', (e) => { - const btn = e.target.closest('.edit-user-roles-settings-btn'); - if (btn) { - openEditUserRolesModal(btn.dataset.id, btn.dataset.username, btn.dataset.avatar); + const btnRole = e.target.closest('.edit-user-roles-settings-btn'); + if (btnRole) { + openEditUserRolesModal(btnRole.dataset.id, btnRole.dataset.username, btnRole.dataset.avatar); + } + const btnBadge = e.target.closest('.edit-user-badges-settings-btn'); + if (btnBadge) { + openEditUserBadgesModal(btnBadge.dataset.id, btnBadge.dataset.username, btnBadge.dataset.avatar); } }); @@ -2162,6 +2205,210 @@ document.addEventListener('DOMContentLoaded', () => { } }); + const badgesTabBtn = document.getElementById('badges-tab-btn'); + const badgesList = document.getElementById('badges-list'); + const addBadgeBtn = document.getElementById('add-badge-btn'); + const saveBadgeBtn = document.getElementById('save-badge-btn'); + const badgeImageUploadInput = document.getElementById('badge-image-upload-input'); + + badgesTabBtn?.addEventListener('click', loadBadges); + + async function loadBadges() { + try { + const resp = await fetch(`api_v1_badges.php?server_id=${activeServerId}`); + const data = await resp.json(); + if (data.success) { + renderBadges(data.badges); + } + } catch (e) { console.error(e); } + } + + function renderBadges(badges) { + if (!badgesList) return; + badgesList.innerHTML = ''; + if (badges.length === 0) { + badgesList.innerHTML = '
Aucun badge créé.
'; + return; + } + badges.forEach(badge => { + const item = document.createElement('div'); + item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded'; + item.innerHTML = ` +
+ + ${escapeHTML(badge.name)} +
+
+ + +
+ `; + badgesList.appendChild(item); + }); + } + + addBadgeBtn?.addEventListener('click', () => { + document.getElementById('edit-badge-id').value = ''; + document.getElementById('edit-badge-name').value = ''; + document.getElementById('edit-badge-image-url').value = ''; + document.getElementById('badge-image-preview').style.backgroundImage = 'none'; + + const modal = new bootstrap.Modal(document.getElementById('badgeEditorModal')); + modal.show(); + }); + + badgesList?.addEventListener('click', (e) => { + if (e.target.classList.contains('edit-badge-btn')) { + const b = e.target.dataset; + document.getElementById('edit-badge-id').value = b.id; + document.getElementById('edit-badge-name').value = b.name; + document.getElementById('edit-badge-image-url').value = b.url; + document.getElementById('badge-image-preview').style.backgroundImage = `url('${b.url}')`; + + const modal = new bootstrap.Modal(document.getElementById('badgeEditorModal')); + modal.show(); + } else if (e.target.classList.contains('delete-badge-btn')) { + if (confirm('Supprimer ce badge ?')) { + const id = e.target.dataset.id; + fetch('api_v1_badges.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'delete', server_id: activeServerId, id }) + }).then(r => r.json()).then(data => { + if (data.success) loadBadges(); + }); + } + } + }); + + badgeImageUploadInput?.addEventListener('change', async () => { + const file = badgeImageUploadInput.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('badge_image', file); + formData.append('server_id', activeServerId); + + try { + const resp = await fetch('api/upload_badge_image.php', { + method: 'POST', + body: formData + }); + const data = await resp.json(); + if (data.success) { + document.getElementById('edit-badge-image-url').value = data.url; + document.getElementById('badge-image-preview').style.backgroundImage = `url('${data.url}')`; + } else { + alert(data.error || 'Erreur d\'upload'); + } + } catch (e) { console.error(e); } + }); + + saveBadgeBtn?.addEventListener('click', async () => { + const id = document.getElementById('edit-badge-id').value; + const name = document.getElementById('edit-badge-name').value; + const image_url = document.getElementById('edit-badge-image-url').value; + + if (!name || !image_url) { + alert('Le nom et l\'image sont requis.'); + return; + } + + try { + const action = id ? 'update' : 'create'; + const resp = await fetch('api_v1_badges.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action, server_id: activeServerId, id, name, image_url }) + }); + const data = await resp.json(); + if (data.success) { + bootstrap.Modal.getInstance(document.getElementById('badgeEditorModal')).hide(); + loadBadges(); + } + } catch (e) { console.error(e); } + }); + + // User Badges Assignment Logic + async function openEditUserBadgesModal(userId, username, avatar) { + document.getElementById('edit-user-badges-user-id').value = userId; + document.getElementById('edit-user-badges-username').textContent = username; + document.getElementById('edit-user-badges-avatar').style.backgroundImage = avatar ? `url('${avatar}')` : 'none'; + + const list = document.getElementById('user-badges-selection-list'); + list.innerHTML = '
Chargement...
'; + + const bsModal = new bootstrap.Modal(document.getElementById('editUserBadgesModal')); + bsModal.show(); + + try { + const [badgeResp, memberBadgeResp] = await Promise.all([ + fetch(`api_v1_badges.php?server_id=${activeServerId}`), + fetch(`api_v1_roles.php?server_id=${activeServerId}`) // We need member info + ]); + + const badgeData = await badgeResp.json(); + const memberData = await memberBadgeResp.json(); + + if (badgeData.success && memberData.success) { + const member = memberData.members.find(m => m.id == userId); + // We'll use a hack to get user badges if not provided by roles API yet, + // but actually I updated api_v1_roles.php to include badge_urls. + // Wait, badge_urls are URLs, but I need IDs for the checkboxes. + + // Let's refine api_v1_roles.php to also return badge_ids. + // Or I can fetch it specifically if needed. + // For now, I'll fetch it from a dedicated endpoint if I add it, + // but let's just update api_v1_roles.php one more time. + + const assignedBadgeUrls = member && member.badge_data ? member.badge_data.split(':::').map(d => d.split('|')[1]) : []; + + list.innerHTML = ''; + badgeData.badges.forEach(badge => { + const isChecked = assignedBadgeUrls.includes(badge.image_url); + const item = document.createElement('div'); + item.className = 'list-group-item bg-dark text-white border-secondary p-2 d-flex align-items-center'; + item.innerHTML = ` + + + `; + list.appendChild(item); + }); + + if (badgeData.badges.length === 0) { + list.innerHTML = '
Aucun badge défini pour ce serveur.
'; + } + } + } catch (e) { console.error(e); } + } + + document.getElementById('save-user-badges-btn')?.addEventListener('click', async () => { + const btn = document.getElementById('save-user-badges-btn'); + const userId = document.getElementById('edit-user-badges-user-id').value; + const badgeIds = Array.from(document.querySelectorAll('.user-badge-checkbox:checked')).map(cb => cb.value); + + try { + const resp = await fetch('api_v1_badges.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'set_user_badges', + server_id: activeServerId, + user_id: userId, + badge_ids: badgeIds + }) + }); + const data = await resp.json(); + if (data.success) { + bootstrap.Modal.getInstance(document.getElementById('editUserBadgesModal')).hide(); + loadRoles(); // Refresh members list + } + } catch (e) { console.error(e); } + }); + // Forum: New Thread const newThreadBtn = document.getElementById('new-thread-btn'); const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null; @@ -2873,6 +3120,17 @@ document.addEventListener('DOMContentLoaded', () => { ` : ''; + let userBadgesHtml = ''; + const bData = msg.badge_data || ''; + if (bData) { + bData.split(':::').forEach(d => { + const parts = d.split('|'); + const name = parts[0]; + const url = parts[1]; + userBadgesHtml += ``; + }); + } + const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g'); const mentionHtml = `@${window.currentUsername}`; const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml); @@ -2884,6 +3142,7 @@ document.addEventListener('DOMContentLoaded', () => { ${escapeHTML(msg.username)} ${renderRoleIconJS(msg.role_icon, '14px')} + ${userBadgesHtml} ${msg.timestamp || 'Just now'} ${pinnedBadge} diff --git a/assets/pasted-20260220-145350-f3302486.png b/assets/pasted-20260220-145350-f3302486.png new file mode 100644 index 0000000..d3b9c1c Binary files /dev/null and b/assets/pasted-20260220-145350-f3302486.png differ diff --git a/db/migrations/20260220_create_badges.sql b/db/migrations/20260220_create_badges.sql new file mode 100644 index 0000000..f37b3a5 --- /dev/null +++ b/db/migrations/20260220_create_badges.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS server_badges ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + image_url TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS member_badges ( + server_id INT NOT NULL, + user_id INT NOT NULL, + badge_id INT NOT NULL, + PRIMARY KEY (server_id, user_id, badge_id), + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (badge_id) REFERENCES server_badges(id) ON DELETE CASCADE +); diff --git a/index.php b/index.php index ff79987..9d9b577 100644 --- a/index.php +++ b/index.php @@ -350,12 +350,13 @@ if ($is_dm_view) { SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url, u.status, (SELECT GROUP_CONCAT(r.id) FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ?) as role_ids, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, - (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon + (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data FROM users u JOIN server_members sm ON u.id = sm.user_id WHERE sm.server_id = ? "); - $stmt->execute([$active_server_id, $active_server_id, $active_server_id, $active_server_id]); + $stmt->execute([$active_server_id, $active_server_id, $active_server_id, $active_server_id, $active_server_id]); $all_server_members = $stmt->fetchAll(); $members = []; @@ -1223,15 +1224,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; Membres —
-
-
"> - +
+
"> +
"> - +
@@ -1881,6 +1882,9 @@ document.addEventListener('DOMContentLoaded', () => { + @@ -1970,6 +1974,15 @@ document.addEventListener('DOMContentLoaded', () => {
+
+
+
Badges du serveur
+ +
+
+ +
+
Webhooks
@@ -2978,5 +2991,61 @@ document.addEventListener('DOMContentLoaded', () => { } + + + + + +