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) ? `
-
Roles
- ` : ''}
+
+ ${(window.isServerOwner || window.canManageServer) ? `
+ Roles
+ Badges
+ ` : ''}
+
`;
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)}
+
+
+ Modifier
+ ×
+
+ `;
+ 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 = `
+
+
+
+ ${escapeHTML(badge.name)}
+
+ `;
+ 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', () => {
Rôles
+
+ Badges
+
Webhooks
@@ -1970,6 +1974,15 @@ document.addEventListener('DOMContentLoaded', () => {
+
+
+
Badges du serveur
+ + Ajouter un badge
+
+
+
+
+
Webhooks
@@ -2978,5 +2991,61 @@ document.addEventListener('DOMContentLoaded', () => {
}
+
+
+
+
+
+
+
+
+
+
+
+ Importer une image
+
+
+
+
+ Nom du badge
+
+
+
+
+
+
+
+
+
+
+