diff --git a/static/js/03-admin.js b/static/js/03-admin.js index 0abccee..0a47222 100644 --- a/static/js/03-admin.js +++ b/static/js/03-admin.js @@ -6,7 +6,7 @@ window.Glow = window.Glow || {}; Glow.closeAdminPanel = () => { Glow.els.adminPanel.classList.remove("open"); Glow.els.adminPanel.setAttribute("aria-hidden", "true"); - Glow.els.adminFab.focus(); + Glow.els.adminFab?.focus(); }; Glow.setAdminTab = tab => { @@ -29,16 +29,12 @@ Glow.setAdminTab = tab => { Glow.els.adminTeamsPanel?.classList.toggle("hidden", !isTeams); }; -Glow.buildAdminRow = ({ - title, - subtitle, - actions = [], - className = "" -}) => { +Glow.buildAdminRow = ({ title, subtitle, actions = [], className = "" }) => { const row = document.createElement("div"); row.className = className || "admin-room"; const details = document.createElement("div"); + const name = document.createElement("div"); name.className = className.includes("user") @@ -79,13 +75,13 @@ Glow.renderAdminUsers = users => { Glow.els.adminUsers.replaceChildren(); const heading = document.createElement("h3"); - heading.textContent = "Users"; + heading.textContent = "users"; Glow.els.adminUsers.append(heading); if (!users.length) { const empty = document.createElement("p"); empty.className = "admin-empty"; - empty.textContent = "No users found."; + empty.textContent = "no users found."; Glow.els.adminUsers.append(empty); return; } @@ -102,7 +98,7 @@ Glow.renderAdminUsers = users => { role.className = "small-button"; role.type = "button"; role.textContent = - user.role === "moderator" ? "Make user" : "Make moderator"; + user.role === "moderator" ? "make user" : "make moderator"; role.addEventListener("click", () => { Glow.updateAdminUser(user.id, { role: user.role === "moderator" ? "user" : "moderator" @@ -113,7 +109,7 @@ Glow.renderAdminUsers = users => { restrict.className = "small-button"; restrict.type = "button"; restrict.textContent = - user.status === "restricted" ? "Unrestrict" : "Restrict"; + user.status === "restricted" ? "unrestrict" : "restrict"; restrict.addEventListener("click", () => { Glow.toggleRestriction(user); }); @@ -122,7 +118,7 @@ Glow.renderAdminUsers = users => { blacklist.className = "small-button danger"; blacklist.type = "button"; blacklist.textContent = - user.status === "blacklisted" ? "Unblacklist" : "Blacklist"; + user.status === "blacklisted" ? "unblacklist" : "blacklist"; blacklist.addEventListener("click", () => { Glow.toggleBlacklist(user); }); @@ -132,11 +128,11 @@ Glow.renderAdminUsers = users => { Glow.els.adminUsers.append( Glow.buildAdminRow({ - title: `${user.username} · ${user.role}`, + title: `${user.username} · ${user.role}`, subtitle: - `${user.status} · ${user.message_count} message(s)` + + `${user.status} · ${user.message_count} message(s)` + (user.restricted_until - ? ` · until ${new Date(user.restricted_until).toLocaleString()}` + ? ` · until ${new Date(user.restricted_until).toLocaleString()}` : ""), actions, className: "admin-user" @@ -149,13 +145,13 @@ Glow.renderAdminRooms = rooms => { Glow.els.adminRooms.replaceChildren(); const heading = document.createElement("h3"); - heading.textContent = "Rooms"; + heading.textContent = "rooms"; Glow.els.adminRooms.append(heading); if (!rooms.length) { const empty = document.createElement("p"); empty.className = "admin-empty"; - empty.textContent = "No rooms found."; + empty.textContent = "no rooms found."; Glow.els.adminRooms.append(empty); return; } @@ -167,7 +163,7 @@ Glow.renderAdminRooms = rooms => { const edit = document.createElement("button"); edit.className = "small-button"; edit.type = "button"; - edit.textContent = "Edit"; + edit.textContent = "edit"; edit.addEventListener("click", () => Glow.editRoom(room)); actions.push(edit); @@ -175,7 +171,7 @@ Glow.renderAdminRooms = rooms => { const members = document.createElement("button"); members.className = "small-button"; members.type = "button"; - members.textContent = "Members"; + members.textContent = "members"; members.addEventListener("click", () => { Glow.manageRoomMembers(room); }); @@ -183,7 +179,7 @@ Glow.renderAdminRooms = rooms => { const invite = document.createElement("button"); invite.className = "small-button"; invite.type = "button"; - invite.textContent = "Invite link"; + invite.textContent = "invite link"; invite.addEventListener("click", () => { Glow.createInviteLink(room); }); @@ -195,7 +191,7 @@ Glow.renderAdminRooms = rooms => { const remove = document.createElement("button"); remove.className = "small-button danger"; remove.type = "button"; - remove.textContent = "Delete"; + remove.textContent = "delete"; remove.addEventListener("click", () => Glow.deleteRoom(room)); actions.push(remove); } @@ -203,10 +199,8 @@ Glow.renderAdminRooms = rooms => { Glow.els.adminRooms.append( Glow.buildAdminRow({ - title: `${room.name} · ${room.visibility}`, - subtitle: - `/${room.slug}` + - (room.description ? ` · ${room.description}` : ""), + title: `${room.name} · ${room.visibility}`, + subtitle: `/${room.slug}` + (room.description ? ` · ${room.description}` : ""), actions, className: "admin-room" }) @@ -220,7 +214,7 @@ Glow.renderAdminTeamsChannels = () => { box.replaceChildren(); const heading = document.createElement("h3"); - heading.textContent = "Teams / channels"; + heading.textContent = "teams / channels"; box.append(heading); const team = Glow.state.activeTeam; @@ -229,7 +223,7 @@ Glow.renderAdminTeamsChannels = () => { if (!team) { const empty = document.createElement("p"); empty.className = "admin-empty"; - empty.textContent = "Select a team to manage team/channel actions."; + empty.textContent = "select a team to manage team or channel actions."; box.append(empty); return; } @@ -239,25 +233,25 @@ Glow.renderAdminTeamsChannels = () => { const editTeam = document.createElement("button"); editTeam.className = "small-button"; editTeam.type = "button"; - editTeam.textContent = "Edit team"; + editTeam.textContent = "edit team"; editTeam.addEventListener("click", () => Glow.openEditTeamDialog?.(team)); const members = document.createElement("button"); members.className = "small-button"; members.type = "button"; - members.textContent = "Members"; + members.textContent = "members"; members.addEventListener("click", () => Glow.manageTeamMembers?.(team)); const invite = document.createElement("button"); invite.className = "small-button"; invite.type = "button"; - invite.textContent = "Invite"; + invite.textContent = "invite"; invite.addEventListener("click", () => Glow.createTeamInviteLink?.(team)); const addChannel = document.createElement("button"); addChannel.className = "small-button"; addChannel.type = "button"; - addChannel.textContent = "Add channel"; + addChannel.textContent = "add channel"; addChannel.addEventListener("click", () => Glow.openCreateChannelDialog?.(team)); teamActions.push(editTeam, members, invite, addChannel); @@ -273,11 +267,10 @@ Glow.renderAdminTeamsChannels = () => { ); const channels = Array.isArray(Glow.state.channels) ? Glow.state.channels : []; - if (!channels.length) { const empty = document.createElement("p"); empty.className = "admin-empty"; - empty.textContent = "No channels available for this team."; + empty.textContent = "no channels available for this team."; box.append(empty); return; } @@ -290,7 +283,7 @@ Glow.renderAdminTeamsChannels = () => { const edit = document.createElement("button"); edit.className = "small-button"; edit.type = "button"; - edit.textContent = "Edit"; + edit.textContent = "edit"; edit.addEventListener("click", () => { Glow.openEditChannelDialog?.(team, item); }); @@ -300,7 +293,7 @@ Glow.renderAdminTeamsChannels = () => { const remove = document.createElement("button"); remove.className = "small-button danger"; remove.type = "button"; - remove.textContent = "Delete"; + remove.textContent = "delete"; remove.addEventListener("click", async () => { const previous = Glow.state.activeChannel; Glow.state.activeChannel = item; @@ -313,7 +306,7 @@ Glow.renderAdminTeamsChannels = () => { box.append( Glow.buildAdminRow({ - title: `${isActive ? "• " : ""}# ${item.name}`, + title: `${isActive ? "• " : ""}# ${item.name}`, subtitle: item.description || `/${item.slug}`, actions, className: "admin-team-row" @@ -329,13 +322,13 @@ Glow.openAdminPanel = async () => { Glow.els.adminPanel.setAttribute("aria-hidden", "false"); if (Glow.els.adminUsers) { - Glow.els.adminUsers.textContent = "Loading users…"; + Glow.els.adminUsers.textContent = "loading users…"; } if (Glow.els.adminRooms) { - Glow.els.adminRooms.textContent = "Loading rooms…"; + Glow.els.adminRooms.textContent = "loading rooms…"; } if (Glow.els.adminTeamsChannels) { - Glow.els.adminTeamsChannels.textContent = "Loading teams/channels…"; + Glow.els.adminTeamsChannels.textContent = "loading teams/channels…"; } try { @@ -347,127 +340,14 @@ Glow.openAdminPanel = async () => { Glow.renderAdminUsers(usersData.users || []); Glow.renderAdminRooms(roomsData.rooms || []); Glow.renderAdminTeamsChannels(); - Glow.setAdminTab(Glow.state.adminTab || "rooms"); - setTimeout(() => Glow.els.closeAdminButton.focus(), 0); + setTimeout(() => Glow.els.closeAdminButton?.focus(), 0); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not load moderation data.", "error"); } }; -Glow.updateThemeButton = () => { - if (!Glow.els.themeButton) return; - Glow.els.themeButton.textContent = "Accent color"; -}; - -Glow.updateAdminPreferenceButtons = () => {}; - -Glow.openAccentColorPicker = async () => { - const container = document.createElement("div"); - container.className = "accent-picker"; - - const description = document.createElement("p"); - description.className = "accent-picker-description"; - description.textContent = - "Choose an accent color for focus rings, buttons, selected controls, and highlighted mentions. It is stored only in this browser unless saved in account settings."; - - const label = document.createElement("label"); - label.className = "accent-picker-label"; - label.textContent = "Accent color"; - - const row = document.createElement("div"); - row.className = "accent-picker-row"; - - const colorInput = document.createElement("input"); - colorInput.className = "accent-color-input"; - colorInput.type = "color"; - colorInput.value = - Glow.normalizeAccentColor(Glow.state.accentColor) || - Glow.DEFAULT_ACCENT_COLOR; - colorInput.setAttribute("aria-label", "Choose accent color"); - - const hexInput = document.createElement("input"); - hexInput.className = "field accent-hex-input"; - hexInput.type = "text"; - hexInput.value = colorInput.value; - hexInput.maxLength = 7; - hexInput.autocomplete = "off"; - hexInput.autocapitalize = "off"; - hexInput.spellcheck = false; - hexInput.placeholder = "#f7dce8"; - hexInput.setAttribute( - "aria-label", - "Accent color hexadecimal value" - ); - - const preview = document.createElement("span"); - preview.className = "accent-preview"; - preview.style.background = colorInput.value; - preview.setAttribute("aria-hidden", "true"); - - const error = document.createElement("div"); - error.className = "accent-picker-error"; - error.setAttribute("role", "status"); - - const updatePreview = value => { - const normalized = Glow.normalizeAccentColor(value); - - if (!normalized) { - preview.style.background = "transparent"; - error.textContent = - "Use a six-digit hex color, such as #7dd3fc."; - return false; - } - - preview.style.background = normalized; - error.textContent = ""; - - if (colorInput.value !== normalized) { - colorInput.value = normalized; - } - - return true; - }; - - colorInput.addEventListener("input", () => { - const color = Glow.normalizeAccentColor(colorInput.value); - if (!color) return; - hexInput.value = color; - updatePreview(color); - }); - - hexInput.addEventListener("input", () => { - updatePreview(hexInput.value.trim()); - }); - - row.append(colorInput, hexInput, preview); - label.append(row); - container.append(description, label, error); - - const result = await Glow.openDialog({ - title: "Accent color", - body: container, - confirmText: "Save color", - cancelText: "Cancel" - }); - - if (!result) return; - - const accent = Glow.normalizeAccentColor(hexInput.value); - - if (!accent) { - Glow.showToast( - "Choose a valid six-digit hexadecimal color.", - "error" - ); - return; - } - - Glow.applyAccentColor(accent); - Glow.showToast("Accent color updated."); -}; - Glow.toggleRestriction = async user => { if (user.status === "restricted") { return Glow.updateAdminUser(user.id, { @@ -477,14 +357,14 @@ Glow.toggleRestriction = async user => { } const values = await Glow.openDialog({ - title: `Restrict ${user.username}`, - body: "Enter the restriction duration in minutes.", - confirmText: "Restrict", + title: `restrict ${user.username}`, + body: "enter the restriction duration in minutes.", + confirmText: "restrict", danger: true, fields: [ { name: "minutes", - label: "Duration in minutes", + label: "duration in minutes", type: "number", value: "60", maxLength: 5 @@ -495,20 +375,14 @@ Glow.toggleRestriction = async user => { if (!values) return; const minutes = Number(values.minutes); - if (!Number.isFinite(minutes) || minutes < 1 || minutes > 43200) { - Glow.showToast( - "Enter a duration between 1 minute and 30 days.", - "error" - ); + Glow.showToast("enter a duration between 1 minute and 30 days.", "error"); return; } await Glow.updateAdminUser(user.id, { status: "restricted", - restricted_until: new Date( - Date.now() + minutes * 60 * 1000 - ).toISOString() + restricted_until: new Date(Date.now() + minutes * 60 * 1000).toISOString() }); }; @@ -517,12 +391,12 @@ Glow.toggleBlacklist = async user => { const result = await Glow.openDialog({ title: blacklisting - ? `Blacklist ${user.username}?` - : `Unblacklist ${user.username}?`, + ? `blacklist ${user.username}?` + : `unblacklist ${user.username}?`, body: blacklisting - ? "The account will be denied access immediately." - : "The account will be restored to active status.", - confirmText: blacklisting ? "Blacklist" : "Unblacklist", + ? "the account will be denied access immediately." + : "the account will be restored to active status.", + confirmText: blacklisting ? "blacklist" : "unblacklist", danger: blacklisting }); @@ -541,36 +415,36 @@ Glow.updateAdminUser = async (userId, changes) => { body: JSON.stringify(changes) }); - Glow.showToast("User updated."); + Glow.showToast("user updated."); await Glow.openAdminPanel(); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not update user.", "error"); } }; Glow.createRoom = async () => { const values = await Glow.openDialog({ - title: "Create room", - body: "Choose a room name, description, and visibility.", - confirmText: "Create", + title: "create room", + body: "choose a room name, description, and visibility.", + confirmText: "create", fields: [ { name: "name", - label: "Room name", - placeholder: "Project chat", + label: "room name", + placeholder: "project chat", maxLength: 60 }, { name: "description", - label: "Description", - placeholder: "Optional room description", + label: "description", + placeholder: "optional room description", multiline: true, rows: 3, maxLength: 300 }, { name: "visibility", - label: "Visibility: public or private", + label: "visibility: public or private", value: "public", maxLength: 7 } @@ -579,46 +453,41 @@ Glow.createRoom = async () => { if (!values) return; - const visibility = values.visibility.toLowerCase(); - + const visibility = String(values.visibility || "").toLowerCase(); if (!["public", "private"].includes(visibility)) { - Glow.showToast("Visibility must be public or private.", "error"); + Glow.showToast("visibility must be public or private.", "error"); return; } try { - await Glow.apiRequest("/api/rooms", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: values.name, - description: values.description, - visibility - }) + await Glow.apiCreateRoom({ + name: values.name, + description: values.description, + visibility }); - Glow.showToast("Room created."); + Glow.showToast("room created."); await Glow.loadRooms(); await Glow.openAdminPanel(); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not create room.", "error"); } }; Glow.editRoom = async room => { const values = await Glow.openDialog({ - title: `Edit ${room.name}`, - confirmText: "Save", + title: `edit ${room.name}`, + confirmText: "save", fields: [ { name: "name", - label: "Room name", + label: "room name", value: room.name, maxLength: 60 }, { name: "description", - label: "Description", + label: "description", value: room.description || "", multiline: true, rows: 3, @@ -626,7 +495,7 @@ Glow.editRoom = async room => { }, { name: "visibility", - label: "Visibility: public or private", + label: "visibility: public or private", value: room.visibility, maxLength: 7 } @@ -635,10 +504,9 @@ Glow.editRoom = async room => { if (!values) return; - const visibility = values.visibility.toLowerCase(); - + const visibility = String(values.visibility || "").toLowerCase(); if (!["public", "private"].includes(visibility)) { - Glow.showToast("Visibility must be public or private.", "error"); + Glow.showToast("visibility must be public or private.", "error"); return; } @@ -651,13 +519,8 @@ Glow.editRoom = async room => { Glow.updateRoom = async (slug, changes) => { try { - await Glow.apiRequest(`/api/rooms/${encodeURIComponent(slug)}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(changes) - }); - - Glow.showToast("Room updated."); + await Glow.apiUpdateRoom(slug, changes); + Glow.showToast("room updated."); await Glow.loadRooms(); if (!Glow.state.activeDm) { @@ -666,15 +529,15 @@ Glow.updateRoom = async (slug, changes) => { await Glow.openAdminPanel(); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not update room.", "error"); } }; Glow.deleteRoom = async room => { const result = await Glow.openDialog({ - title: `Delete ${room.name}?`, - body: "This permanently deletes the room and every message in it.", - confirmText: "Delete room", + title: `delete ${room.name}?`, + body: "this permanently deletes the room and every message in it.", + confirmText: "delete room", danger: true }); @@ -685,7 +548,7 @@ Glow.deleteRoom = async room => { method: "DELETE" }); - Glow.showToast("Room deleted."); + Glow.showToast("room deleted."); await Glow.loadRooms(); if (!Glow.state.activeDm) { @@ -694,43 +557,39 @@ Glow.deleteRoom = async room => { await Glow.openAdminPanel(); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not delete room.", "error"); } }; Glow.clearActiveRoom = async () => { const room = Glow.state.activeRoom; - if (!room || Glow.state.activeDm) return; const result = await Glow.openDialog({ - title: `Clear ${room.name}?`, - body: "All messages in this room will be permanently deleted.", - confirmText: "Clear room", + title: `clear ${room.name}?`, + body: "all messages in this room will be permanently deleted.", + confirmText: "clear room", danger: true }); if (!result) return; try { - await Glow.apiRequest( - `/api/rooms/${encodeURIComponent(room.slug)}/messages`, - { method: "DELETE" } - ); + await Glow.apiRequest(`/api/rooms/${encodeURIComponent(room.slug)}/messages`, { + method: "DELETE" + }); Glow.clearMessages(); Glow.renderEmptyState(); - Glow.showToast("Room messages cleared."); + Glow.showToast("room messages cleared."); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not clear room.", "error"); } }; Glow.manageRoomMembers = async room => { try { - const data = await Glow.apiRequest( - `/api/rooms/${encodeURIComponent(room.slug)}/members` - ); + const data = await Glow.apiRequest(`/api/rooms/${encodeURIComponent(room.slug)}/members`); const container = document.createElement("div"); container.className = "member-list"; @@ -740,12 +599,12 @@ Glow.manageRoomMembers = async room => { row.className = "member-row"; const name = document.createElement("span"); - name.textContent = `${member.username} · ${member.membership_role}`; + name.textContent = `${member.username} · ${member.membership_role}`; const remove = document.createElement("button"); remove.className = "small-button danger"; remove.type = "button"; - remove.textContent = "Remove"; + remove.textContent = "remove"; remove.disabled = member.membership_role === "owner"; remove.addEventListener("click", async () => { @@ -754,12 +613,11 @@ Glow.manageRoomMembers = async room => { `/api/rooms/${encodeURIComponent(room.slug)}/members/${member.id}`, { method: "DELETE" } ); - - Glow.showToast("Member removed."); + Glow.showToast("member removed."); Glow.closeDialog({}); Glow.manageRoomMembers(room); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not remove member.", "error"); } }); @@ -770,11 +628,11 @@ Glow.manageRoomMembers = async room => { const values = await Glow.openDialog({ title: `${room.name} members`, body: container, - confirmText: "Add member", + confirmText: "add member", fields: [ { name: "username", - label: "Username to add", + label: "username to add", placeholder: "username", maxLength: 30 } @@ -783,46 +641,37 @@ Glow.manageRoomMembers = async room => { if (!values?.username) return; - await Glow.apiRequest( - `/api/rooms/${encodeURIComponent(room.slug)}/members`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username: values.username }) - } - ); + await Glow.apiRequest(`/api/rooms/${encodeURIComponent(room.slug)}/members`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: values.username }) + }); - Glow.showToast("Member added."); + Glow.showToast("member added."); await Glow.manageRoomMembers(room); } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not manage room members.", "error"); } }; Glow.createInviteLink = async room => { try { - const data = await Glow.apiRequest( - `/api/rooms/${encodeURIComponent(room.slug)}/invites`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - } - ); + const data = await Glow.apiRequest(`/api/rooms/${encodeURIComponent(room.slug)}/invites`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + }); - const url = new URL( - data.invite.url, - window.location.origin - ).toString(); + const url = new URL(data.invite.url, window.location.origin).toString(); const values = await Glow.openDialog({ - title: "Private-room invite link", - body: "Copy and share this link. It can be used once before it expires.", - confirmText: "Copy link", + title: "private-room invite link", + body: "copy and share this link. it can be used once before it expires.", + confirmText: "copy link", fields: [ { name: "invite", - label: "Invite link", + label: "invite link", value: url, maxLength: 500 } @@ -831,10 +680,10 @@ Glow.createInviteLink = async room => { if (values) { await navigator.clipboard.writeText(url); - Glow.showToast("Invite link copied."); + Glow.showToast("invite link copied."); } } catch (error) { - Glow.showToast(error.message, "error"); + Glow.showToast(error.message || "could not create invite link.", "error"); } };