diff --git a/api_v1_roles.php b/api_v1_roles.php index c42ae85..7763b81 100644 --- a/api_v1_roles.php +++ b/api_v1_roles.php @@ -121,6 +121,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = db()->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND ur.role_id = ? AND r.server_id = ?"); $stmt->execute([$target_user_id, $role_id, $server_id]); echo json_encode(['success' => true]); + } elseif ($action === 'reorder') { + $orders = $data['orders'] ?? []; + foreach ($orders as $order) { + $stmt = db()->prepare("UPDATE roles SET position = ? WHERE id = ? AND server_id = ?"); + $stmt->execute([$order['position'], $order['id'], $server_id]); + } + echo json_encode(['success' => true]); + } elseif ($action === 'set_user_roles') { + $target_user_id = $data['user_id'] ?? 0; + $role_ids = $data['role_ids'] ?? []; + + // Begin transaction + $db = db(); + $db->beginTransaction(); + try { + // Remove all existing roles for this user in this server + $stmt = $db->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND r.server_id = ?"); + $stmt->execute([$target_user_id, $server_id]); + + // Add new roles + if (!empty($role_ids)) { + $stmt = $db->prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)"); + foreach ($role_ids as $rid) { + // Verify role belongs to server + $check = $db->prepare("SELECT id FROM roles WHERE id = ? AND server_id = ?"); + $check->execute([$rid, $server_id]); + if ($check->fetch()) { + $stmt->execute([$target_user_id, $rid]); + } + } + } + $db->commit(); + echo json_encode(['success' => true]); + } catch (Exception $e) { + $db->rollBack(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } } exit; } diff --git a/assets/css/discord.css b/assets/css/discord.css index 580d42d..db4b44c 100644 --- a/assets/css/discord.css +++ b/assets/css/discord.css @@ -230,10 +230,30 @@ body { } .sortable-ghost { - background-color: var(--hover) !important; - opacity: 0.5; + opacity: 0.4; + background-color: var(--blurple) !important; } +.role-drag-handle:hover { + opacity: 1 !important; + color: var(--blurple); +} + +.member-context-menu { + animation: menuFadeIn 0.1s ease-out; +} + +@keyframes menuFadeIn { + from { opacity: 0; transform: translateY(-5px); } + to { opacity: 1; transform: translateY(0); } +} + +.member-menu-action:hover { + background-color: var(--blurple) !important; + color: white !important; +} + + .add-channel-btn { cursor: pointer; font-size: 1.2em; diff --git a/assets/js/main.js b/assets/js/main.js index 741f6cc..f5ee9f2 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -628,17 +628,63 @@ document.addEventListener('DOMContentLoaded', () => { return; } - // Start DM - const dmBtn = e.target.closest('.start-dm-btn'); - if (dmBtn) { - const userId = dmBtn.dataset.userId; - const formData = new FormData(); - formData.append('user_id', userId); - const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData }); - const result = await resp.json(); - if (result.success) { - window.location.href = `?server_id=dms&channel_id=${result.channel_id}`; - } + // Member Menu + const memberItem = e.target.closest('.member-item'); + if (memberItem) { + const userId = memberItem.dataset.userId; + const username = memberItem.dataset.username; + const avatar = memberItem.dataset.avatar; + + // Create or show member menu + document.querySelector('.member-context-menu')?.remove(); + const menu = document.createElement('div'); + menu.className = 'member-context-menu bg-dark border border-secondary rounded p-2'; + menu.style.position = 'fixed'; + menu.style.zIndex = '1000'; + menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; + menu.style.minWidth = '150px'; + + const rect = memberItem.getBoundingClientRect(); + menu.style.top = `${rect.top}px`; + menu.style.left = `${rect.left - 160}px`; + + menu.innerHTML = ` +
+
+ ${escapeHTML(username)} +
+ + ${(window.isServerOwner || window.canManageServer) ? `` : ''} + `; + + document.body.appendChild(menu); + + // Close menu on click outside + const closeMenu = (e) => { + if (!menu.contains(e.target)) { + menu.remove(); + document.removeEventListener('mousedown', closeMenu); + } + }; + document.addEventListener('mousedown', closeMenu); + + menu.querySelectorAll('.member-menu-action').forEach(btn => { + btn.onclick = async () => { + const action = btn.dataset.action; + if (action === 'message') { + const formData = new FormData(); + formData.append('user_id', userId); + const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData }); + const result = await resp.json(); + if (result.success) { + window.location.href = `?server_id=dms&channel_id=${result.channel_id}`; + } + } else if (action === 'edit-roles') { + openEditUserRolesModal(userId, username, avatar); + } + menu.remove(); + }; + }); } }); @@ -1010,10 +1056,88 @@ document.addEventListener('DOMContentLoaded', () => { serverPermissions = data.permissions_list; if (rolesList) renderRoles(data.roles); if (membersList) renderMembers(data.members); + updateGlobalUI(data.members); } } catch (e) { console.error(e); } } + function renderRoleIconJS(icon, size = '12px') { + if (!icon) return ''; + const isUrl = icon.startsWith('http') || icon.startsWith('/'); + if (isUrl) { + return ``; + } else { + return `${escapeHTML(icon)}`; + } + } + + function updateGlobalUI(members) { + // Update members sidebar + const sidebar = document.querySelector('.members-sidebar'); + if (sidebar) { + const countEl = sidebar.querySelector('div[style*="text-transform: uppercase"]'); + if (countEl) countEl.textContent = `Members — ${members.length}`; + + // We need to keep the "Members - X" div and replace everything else + const header = sidebar.firstElementChild; + sidebar.innerHTML = ''; + sidebar.appendChild(header); + + members.forEach(m => { + const item = document.createElement('div'); + item.className = 'channel-item member-item'; + item.dataset.userId = m.id; + item.dataset.username = m.username; + item.dataset.avatar = m.avatar_url || ''; + item.style.color = 'var(--text-primary)'; + item.style.marginBottom = '8px'; + item.style.cursor = 'pointer'; + + const roleIconHtml = renderRoleIconJS(m.role_icon, '12px'); + const avatarBg = m.avatar_url ? `background-image: url('${m.avatar_url}');` : ''; + const statusColor = m.status === 'online' ? '#23a559' : '#80848e'; + + item.innerHTML = ` +
+ ${m.status === 'online' ? `
` : ''} +
+ + ${escapeHTML(m.username)} + ${roleIconHtml} + + `; + sidebar.appendChild(item); + }); + } + + // Update chat colors + document.querySelectorAll('.message-author').forEach(authorEl => { + const username = authorEl.childNodes[0].textContent.trim(); + const member = members.find(m => m.username === username); + if (member) { + authorEl.style.color = member.role_color || 'inherit'; + // Try to update icon if it exists or add it if it doesn't + let iconEl = authorEl.querySelector('.role-icon, span.ms-1'); + const newIconHtml = renderRoleIconJS(member.role_icon, '12px'); + + if (newIconHtml) { + if (iconEl) { + const temp = document.createElement('div'); + temp.innerHTML = newIconHtml; + iconEl.replaceWith(temp.firstChild); + } else { + const temp = document.createElement('div'); + temp.innerHTML = newIconHtml; + // Insert after the text node + authorEl.insertBefore(temp.firstChild, authorEl.childNodes[1]); + } + } else if (iconEl) { + iconEl.remove(); + } + } + }); + } + function renderRoles(roles) { rolesList.innerHTML = ''; if (roles.length === 0) { @@ -1021,10 +1145,14 @@ document.addEventListener('DOMContentLoaded', () => { } roles.forEach(role => { 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.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded role-sortable-item'; + item.dataset.id = role.id; const isUrl = role.icon_url && (role.icon_url.startsWith('http') || role.icon_url.startsWith('/')); item.innerHTML = `
+
+ +
${role.name} ${role.icon_url ? (isUrl ? `` : `${role.icon_url}`) : ''} @@ -1036,28 +1164,49 @@ document.addEventListener('DOMContentLoaded', () => { `; rolesList.appendChild(item); }); + + // Initialize Sortable for roles + if (typeof Sortable !== 'undefined' && rolesList) { + new Sortable(rolesList, { + animation: 150, + handle: '.role-drag-handle', + ghostClass: 'sortable-ghost', + onEnd: () => saveRolePositions() + }); + } + } + + async function saveRolePositions() { + const orders = []; + const items = rolesList.querySelectorAll('.role-sortable-item'); + // Invert the order because we ORDER BY position DESC in SQL + let position = items.length - 1; + items.forEach(item => { + orders.push({ + id: item.dataset.id, + position: position-- + }); + }); + + try { + await fetch('api_v1_roles.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'reorder', + server_id: activeServerId, + orders: orders + }) + }); + } catch (e) { console.error(e); } } function renderMembers(members) { membersList.innerHTML = ''; members.forEach(member => { - const memberRoles = member.role_ids ? member.role_ids.split(',') : []; 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-2 rounded bg-dark'; - let rolesHtml = ''; - serverRoles.forEach(role => { - const isAssigned = memberRoles.includes(role.id.toString()); - rolesHtml += ` -
- - -
- `; - }); - const isIconUrl = member.role_icon && (member.role_icon.startsWith('http') || member.role_icon.startsWith('/')); const roleIconHtml = member.role_icon ? (isIconUrl ? `` : `${member.role_icon}`) : ''; @@ -1069,16 +1218,28 @@ document.addEventListener('DOMContentLoaded', () => { ${escapeHTML(member.username)} ${roleIconHtml}
-
- ${rolesHtml || 'No roles available'} +
+ ${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
+ ${(window.isServerOwner || window.canManageServer) ? ` + + ` : ''} `; membersList.appendChild(item); }); } + // 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); + } + }); + + // Role Editing Modal Logic rolesList?.addEventListener('click', (e) => { if (e.target.classList.contains('edit-role-btn-v2')) { @@ -1108,7 +1269,9 @@ document.addEventListener('DOMContentLoaded', () => { } }); - document.getElementById('save-role-btn')?.addEventListener('click', async () => { + document.getElementById('save-role-btn')?.addEventListener('click', async (e) => { + const btn = e.target; + const originalText = btn.textContent; const id = document.getElementById('edit-role-id').value; const name = document.getElementById('edit-role-name').value; const color = document.getElementById('edit-role-color').value; @@ -1127,31 +1290,92 @@ document.addEventListener('DOMContentLoaded', () => { }); const data = await resp.json(); if (data.success) { - bootstrap.Modal.getInstance(document.getElementById('roleEditorModal')).hide(); + btn.textContent = 'Saved ✅'; + btn.classList.replace('btn-primary', 'btn-success'); + setTimeout(() => { + btn.textContent = originalText; + btn.classList.replace('btn-success', 'btn-primary'); + }, 2000); loadRoles(); } } catch (e) { console.error(e); } }); - membersList?.addEventListener('change', async (e) => { - if (e.target.classList.contains('role-assign-check')) { - const userId = e.target.dataset.userId; - const roleId = e.target.dataset.roleId; - const action = e.target.checked ? 'assign' : 'unassign'; + async function openEditUserRolesModal(userId, username, avatar) { + const modal = document.getElementById('editUserRolesModal'); + document.getElementById('edit-user-roles-user-id').value = userId; + document.getElementById('edit-user-roles-username').textContent = username; + const avatarEl = document.getElementById('edit-user-roles-avatar'); + avatarEl.style.backgroundImage = avatar ? `url('${avatar}')` : 'none'; + + const list = document.getElementById('user-roles-selection-list'); + list.innerHTML = '
Loading roles...
'; + + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + + try { + // We need to fetch roles and the current user's roles + // We can reuse loadRoles or make a specific call + const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`); + const data = await resp.json(); - try { - const resp = await fetch('api_v1_roles.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action, server_id: activeServerId, user_id: userId, role_id: roleId }) + if (data.success) { + const member = data.members.find(m => m.id == userId); + const assignedRoles = member && member.role_ids ? member.role_ids.split(',') : []; + + list.innerHTML = ''; + // Sort roles by position descending for display + data.roles.sort((a, b) => b.position - a.position).forEach(role => { + const isChecked = assignedRoles.includes(role.id.toString()); + const item = document.createElement('div'); + item.className = 'list-group-item bg-dark text-white border-secondary p-2 d-flex align-items-center'; + item.innerHTML = ` + + + `; + list.appendChild(item); }); - const data = await resp.json(); - if (!data.success) { - alert(data.error || 'Failed to update role'); - e.target.checked = !e.target.checked; + + if (data.roles.length === 0) { + list.innerHTML = '
No roles defined for this server.
'; } - } catch (e) { console.error(e); } - } + } + } catch (e) { console.error(e); } + } + + document.getElementById('save-user-roles-btn')?.addEventListener('click', async (e) => { + const btn = e.target; + const originalText = btn.textContent; + const userId = document.getElementById('edit-user-roles-user-id').value; + const roleIds = Array.from(document.querySelectorAll('.user-role-checkbox:checked')).map(cb => cb.value); + + try { + const resp = await fetch('api_v1_roles.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'set_user_roles', + server_id: activeServerId, + user_id: userId, + role_ids: roleIds + }) + }); + const data = await resp.json(); + if (data.success) { + btn.textContent = 'Saved ✅'; + btn.classList.replace('btn-primary', 'btn-success'); + setTimeout(() => { + btn.textContent = originalText; + btn.classList.replace('btn-success', 'btn-primary'); + }, 2000); + loadRoles(); + } else { + alert(data.error || 'Failed to update roles'); + } + } catch (e) { console.error(e); } }); addRoleBtn?.addEventListener('click', async () => { diff --git a/index.php b/index.php index 7dc3c27..6b54385 100644 --- a/index.php +++ b/index.php @@ -776,7 +776,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; Members — -
+
">
@@ -1379,6 +1379,35 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+ + +