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'] ?? '';
+
+
+
+
+
+
+
+
+
+
Username
+
Select roles to assign to this member
+
+
+
+
+
+
+
+
+
+
+
+