ReleaseV08+Badges

This commit is contained in:
Flatlogic Bot 2026-02-20 15:04:34 +00:00
parent cc765bec94
commit d0cead395d
9 changed files with 555 additions and 25 deletions

View File

@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/../auth/session.php';
require_once __DIR__ . '/../includes/permissions.php';
header('Content-Type: application/json');
$user = getCurrentUser();
if (!$user) {
echo json_encode(['success' => 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']);
}

105
api_v1_badges.php Normal file
View File

@ -0,0 +1,105 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode(['success' => 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;
}

View File

@ -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,

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

View File

@ -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 = `
<div class="mb-2 p-1">
<div class="small text-muted text-uppercase mb-1" style="font-size: 0.6em; font-weight: bold; opacity: 0.8;">Badges</div>
<div class="d-flex flex-wrap gap-1">
${badgeData.map(d => {
const parts = d.split('|');
const name = parts[0];
const url = parts[1];
return `<img src="${url}" style="width: 32px; height: 32px; object-fit: contain;" title="${escapeHTML(name)}">`;
}).join('')}
</div>
</div>
<div class="border-top border-secondary mb-2 mx-1"></div>
`;
}
let rolesHtml = '';
if (roleIds.length > 0) {
// Deduplicate and filter valid roles from serverRoles
@ -979,9 +998,13 @@ document.addEventListener('DOMContentLoaded', () => {
<span class="small fw-bold">${escapeHTML(username)}</span>
</div>
<div class="border-top border-secondary mb-2 mx-1"></div>
${badgesHtml}
${rolesHtml}
<button class="btn btn-sm btn-dark w-100 text-start mb-1 member-menu-action" data-action="message">Message</button>
${(window.isServerOwner || window.canManageServer) ? `<button class="btn btn-sm btn-dark w-100 text-start member-menu-action" data-action="edit-roles">Éditer son rôle</button>` : ''}
${(window.isServerOwner || window.canManageServer) ? `
<button class="btn btn-sm btn-dark w-100 text-start mb-1 member-menu-action" data-action="edit-roles">Éditer son rôle</button>
<button class="btn btn-sm btn-dark w-100 text-start member-menu-action" data-action="edit-badges">Éditer ses badges</button>
` : ''}
`;
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 += `<img src="${url}" class="ms-1" style="width: 32px; height: 32px; object-fit: contain;" title="${escapeHTML(name)}">`;
});
}
item.innerHTML = `
<div class="d-flex align-items-center flex-grow-1">
<div class="message-avatar me-2" style="width: 32px; height: 32px; ${member.avatar_url ? `background-image: url('${member.avatar_url}');` : ''}"></div>
<div class="flex-grow-1">
<div class="fw-bold small" style="color: ${member.role_color || 'inherit'}">
<div class="fw-bold small d-flex align-items-center" style="color: ${member.role_color || 'inherit'}">
${escapeHTML(member.username)}
${roleIconHtml}
<div class="ms-2 d-flex gap-1">${badgesHtml}</div>
</div>
<div class="text-muted small">
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
</div>
</div>
</div>
${(window.isServerOwner || window.canManageServer) ? `
<button class="btn btn-sm btn-outline-light edit-user-roles-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Roles</button>
` : ''}
<div class="d-flex gap-2">
${(window.isServerOwner || window.canManageServer) ? `
<button class="btn btn-sm btn-outline-light edit-user-roles-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Roles</button>
<button class="btn btn-sm btn-outline-light edit-user-badges-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Badges</button>
` : ''}
</div>
`;
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 = '<div class="text-center p-3 text-muted">Aucun badge créé.</div>';
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 = `
<div class="d-flex align-items-center">
<img src="${badge.image_url}" style="width: 32px; height: 32px; object-fit: contain; margin-right: 12px;">
<span class="fw-medium">${escapeHTML(badge.name)}</span>
</div>
<div>
<button class="btn btn-sm btn-outline-light edit-badge-btn" data-id="${badge.id}" data-name="${badge.name}" data-url="${badge.image_url}">Modifier</button>
<button class="btn btn-sm btn-outline-danger delete-badge-btn" data-id="${badge.id}">×</button>
</div>
`;
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 = '<div class="text-center p-3 text-muted">Chargement...</div>';
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 = `
<input class="form-check-input me-3 user-badge-checkbox" type="checkbox" value="${badge.id}" id="user-badge-${badge.id}" ${isChecked ? 'checked' : ''}>
<label class="form-check-label flex-grow-1 d-flex align-items-center" for="user-badge-${badge.id}" style="cursor: pointer;">
<img src="${badge.image_url}" style="width: 32px; height: 32px; object-fit: contain; margin-right: 8px;">
${escapeHTML(badge.name)}
</label>
`;
list.appendChild(item);
});
if (badgeData.badges.length === 0) {
list.innerHTML = '<div class="text-center p-3 text-muted">Aucun badge défini pour ce serveur.</div>';
}
}
} 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', () => {
</span>
` : '';
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 += `<img src="${url}" class="ms-1" style="width: 32px; height: 32px; object-fit: contain; vertical-align: middle;" title="${escapeHTML(name)}">`;
});
}
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
const mentionHtml = `<span class="mention">@${window.currentUsername}</span>`;
const contentWithMentions = parseCustomEmotes(msg.content).replace(mentionRegex, mentionHtml);
@ -2884,6 +3142,7 @@ document.addEventListener('DOMContentLoaded', () => {
<span class="message-username" style="color: ${msg.role_color || 'inherit'};">
${escapeHTML(msg.username)}
${renderRoleIconJS(msg.role_icon, '14px')}
<span class="ms-1 d-inline-flex gap-1">${userBadgesHtml}</span>
</span>
<span class="message-timestamp">${msg.timestamp || 'Just now'}</span>
${pinnedBadge}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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
);

View File

@ -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 <?php echo count($members); ?>
</div>
<?php foreach($members as $m): ?>
<div class="channel-item member-item" data-user-id="<?php echo $m['id']; ?>" data-username="<?php echo htmlspecialchars($m['username']); ?>" data-avatar="<?php echo htmlspecialchars($m['avatar_url'] ?? ''); ?>" data-role-ids="<?php echo $m['role_ids'] ?? ''; ?>" style="color: var(--text-primary); margin-bottom: 8px; cursor: pointer;">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: <?php echo $m['status'] == 'online' ? '#23a559' : '#80848e'; ?>; position: relative; <?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>">
<?php if($m['status'] == 'online'): ?>
<div class="channel-item member-item" data-user-id="<?php echo $m['id']; ?>" data-username="<?php echo htmlspecialchars($m['username']); ?>" data-avatar="<?php echo htmlspecialchars($m['avatar_url'] ?? ''); ?>" data-role-ids="<?php echo $m['role_ids'] ?? ''; ?>" data-badge-data="<?php echo htmlspecialchars($m['badge_data'] ?? ''); ?>" style="color: var(--text-primary); margin-bottom: 8px; cursor: pointer;">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: <?php echo ($m['status'] ?? 'offline') == 'online' ? '#23a559' : '#80848e'; ?>; position: relative; <?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>">
<?php if(($m['status'] ?? 'offline') == '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 !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<?php echo renderRoleIcon($m['role_icon'], '14px'); ?>
<?php echo renderRoleIcon($m['role_icon'] ?? '', '14px'); ?>
</span>
</div>
<?php endforeach; ?>
@ -1881,6 +1882,9 @@ document.addEventListener('DOMContentLoaded', () => {
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="roles-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-roles" type="button">Rôles</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="badges-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-badges" type="button">Badges</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="webhooks-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-webhooks" type="button">Webhooks</button>
</li>
@ -1970,6 +1974,15 @@ document.addEventListener('DOMContentLoaded', () => {
<!-- Roles will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-badges">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Badges du serveur</h6>
<button class="btn btn-sm btn-primary" id="add-badge-btn">+ Ajouter un badge</button>
</div>
<div id="badges-list" class="list-group list-group-flush bg-transparent">
<!-- Badges will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-webhooks">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Webhooks</h6>
@ -2978,5 +2991,61 @@ document.addEventListener('DOMContentLoaded', () => {
}
<?php endif; ?>
</script>
<!-- Badge Editor Modal -->
<div class="modal fade" id="badgeEditorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Créer un badge</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-badge-id">
<div class="mb-3 text-center">
<div id="badge-image-preview" class="mx-auto mb-2" style="width: 64px; height: 64px; background-size: contain; background-repeat: no-repeat; background-position: center; border: 1px dashed var(--secondary); border-radius: 4px;"></div>
<input type="hidden" id="edit-badge-image-url">
<button type="button" class="btn btn-sm btn-outline-light" onclick="document.getElementById('badge-image-upload-input').click()">
<i class="fas fa-upload me-1"></i> Importer une image
</button>
<input type="file" id="badge-image-upload-input" style="display: none;" accept="image/*">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du badge</label>
<input type="text" id="edit-badge-name" class="form-control" placeholder="Ex: Top Donateur">
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary" id="save-badge-btn">Enregistrer</button>
</div>
</div>
</div>
</div>
<!-- Edit User Badges Modal -->
<div class="modal fade" id="editUserBadgesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Gérer les badges de <span id="edit-user-badges-username"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-user-badges-user-id">
<div class="text-center mb-3">
<div id="edit-user-badges-avatar" class="message-avatar mx-auto" style="width: 64px; height: 64px;"></div>
</div>
<div id="user-badges-selection-list" class="list-group list-group-flush bg-transparent overflow-auto" style="max-height: 300px;">
<!-- Badges checkboxes here -->
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="button" class="btn btn-primary" id="save-user-badges-btn">Enregistrer</button>
</div>
</div>
</div>
</div>
</body>
</html>