ReleaseV08+Badges
This commit is contained in:
parent
cc765bec94
commit
d0cead395d
74
api/upload_badge_image.php
Normal file
74
api/upload_badge_image.php
Normal 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
105
api_v1_badges.php
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
assets/images/badges/badge_1_1771598872_8939.jpg
Normal file
BIN
assets/images/badges/badge_1_1771598872_8939.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
@ -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}
|
||||
|
||||
BIN
assets/pasted-20260220-145350-f3302486.png
Normal file
BIN
assets/pasted-20260220-145350-f3302486.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
18
db/migrations/20260220_create_badges.sql
Normal file
18
db/migrations/20260220_create_badges.sql
Normal 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
|
||||
);
|
||||
81
index.php
81
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 — <?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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user