diff --git a/frontend/www/app.js b/frontend/www/app.js deleted file mode 100644 index 0845cd6..0000000 --- a/frontend/www/app.js +++ /dev/null @@ -1,1267 +0,0 @@ -const API_BASE = "/api"; - -function getToken(){ - return localStorage.getItem("token"); -} - -function setToken(token){ - localStorage.setItem("token", token); -} - -function clearToken(){ - localStorage.removeItem("token"); -} - -async function apiFetch(path, options = {}){ - let headers = options.headers ? { ...options.headers } : {}; - headers["Content-Type"] = "application/json"; - - let token = getToken(); - if(token){ - headers["Authorization"] = "Bearer " + token; - } - - let response = await fetch(API_BASE + path, { ...options, headers }); - - if(response.status === 401){ - clearToken(); - showLogin(); - throw new Error("not authenticated"); - } - - let data = await response.json().catch(() => null); - - if(!response.ok){ - let message = (data && data.detail) ? data.detail : "request failed"; - throw new Error(message); - } - - return data; -} - -// Small helper: several endpoints (join-room, search-room, create event, -// delete room) return HTTP 200 with a { status: "failed" | "denied" | ... } -// body instead of a real error status. apiFetch only throws on non-2xx, -// so callers that care about these soft failures check result.status -// themselves — see handleJoinRoom, handleSearchRoom, handleCreateEvent, -// handleDeleteRoom below. - -// ---------- date helpers ---------- - -// datetime-local inputs give "YYYY-MM-DDTHH:MM"; the backend stores -// "YYYY-MM-DD HH:MM" (see the create_event() example in events.py). -function toBackendDateTime(localValue){ - return localValue ? localValue.replace("T", " ") : localValue; -} - -function toDatetimeLocal(value){ - if(!value) return ""; - - let d = new Date(value); - if(isNaN(d.getTime())) return ""; - - let pad = n => String(n).padStart(2, "0"); - - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; -} - -// ---------- auth ---------- - -function showLogin(){ - document.getElementById("app").style.display = "none"; - document.getElementById("login-screen").style.display = "flex"; -} - -function showApp(){ - document.getElementById("login-screen").style.display = "none"; - document.getElementById("app").style.display = "grid"; -} - -async function handleLogin(event){ - event.preventDefault(); - - let username = document.getElementById("login-username").value; - let password = document.getElementById("login-password").value; - let errorBox = document.getElementById("login-error"); - - errorBox.textContent = ""; - - try{ - let result = await apiFetch("/login", { - method: "POST", - body: JSON.stringify({ username, password }) - }); - - if(result.status !== "success"){ - errorBox.textContent = "invalid username or password"; - return; - } - - setToken(result.access_token); - showApp(); - showPage("dashboard"); - - } catch(error){ - errorBox.textContent = error.message; - } -} - -async function handleRegister(event){ - event.preventDefault(); - - let username = document.getElementById("login-username").value; - let password = document.getElementById("login-password").value; - let errorBox = document.getElementById("login-error"); - - errorBox.textContent = ""; - - try{ - await apiFetch("/create-user", { - method: "POST", - body: JSON.stringify({ username, password }) - }); - - errorBox.style.color = "green"; - errorBox.textContent = "account created, you can log in now"; - - } catch(error){ - errorBox.style.color = ""; - errorBox.textContent = error.message; - } -} - -function logout(){ - clearToken(); - showLogin(); -} - -// ---------- page router ---------- - -function showPage(page){ - let content = document.getElementById("content"); - - document.querySelectorAll("nav a").forEach(link => link.classList.remove("active")); - - let activeLink = document.querySelector(`nav a[data-page="${page}"]`); - if(activeLink){ - activeLink.classList.add("active"); - } - - if(page === "dashboard") loadDashboard(); - if(page === "rooms") loadRooms(); - if(page === "events") loadEvents(); - - if(page === "settings"){ - renderSettingsPage(content); - } -} - -// ---------- dashboard ---------- - -let calendarWeekOffset = 0; -let cachedDashboardEvents = []; -let cachedRoomsForForm = []; -let currentUserId = null; - -async function loadDashboard(){ - let content = document.getElementById("content"); - - content.innerHTML = ` -

Dashboard

-
Loading...
- `; - - calendarWeekOffset = 0; - - try{ - let me = await apiFetch("/me"); - currentUserId = me.id; - - let roomsData = await apiFetch("/rooms"); - let rooms = roomsData.rooms || []; - cachedRoomsForForm = rooms; - - let eventsData = await apiFetch("/events"); - cachedDashboardEvents = eventsData.events || []; - - let roomsHtml = rooms.length - ? rooms.map(room => `
${room.name}
`).join("") - : `

You have no rooms yet.

`; - - content.innerHTML = ` -

Dashboard

- -
-

Welcome back, ${me.username}

-

Overview of your calendar.

-
- -
- -
- -
- ${buildWeekCalendarHtml(cachedDashboardEvents, calendarWeekOffset)} -
- -
-

Your rooms

- ${roomsHtml} -
- -
-

Create a room

-
- - - -
-

-
- -
-

Join a room

-
- - -
-

-
- `; - - } catch(error){ - content.innerHTML = ` -

Dashboard

-
Could not load dashboard: ${error.message}
- `; - } -} - -async function handleCreateRoom(event){ - event.preventDefault(); - - let roomName = document.getElementById("create-room-name").value; - let inviteCode = document.getElementById("create-room-code").value; - let messageBox = document.getElementById("create-room-message"); - - try{ - await apiFetch("/create-room", { - method: "POST", - body: JSON.stringify({ room_name: roomName, invite_code: inviteCode }) - }); - - messageBox.textContent = "room created"; - loadDashboard(); - - } catch(error){ - messageBox.textContent = error.message; - } -} - -async function handleJoinRoom(event){ - event.preventDefault(); - - let inviteCode = document.getElementById("join-room-code").value; - let messageBox = document.getElementById("join-room-message"); - - try{ - let result = await apiFetch("/join-room", { - method: "POST", - body: JSON.stringify({ invite_code: inviteCode }) - }); - - // /join-room returns 200 with status:"failed" for both an - // unknown code AND an already-joined room - it can't be told apart - // client-side, so the message stays deliberately generic. - if(result.status === "failed"){ - messageBox.textContent = "invalid invite code, or you're already in that room"; - return; - } - - messageBox.textContent = "joined room"; - loadDashboard(); - - } catch(error){ - messageBox.textContent = error.message; - } -} - -// ---------- calendar (week view) ---------- - -const CALENDAR_SLOT_HOURS = 2; -const CALENDAR_SLOT_HEIGHT = 48; -const CALENDAR_START_HOUR = 0; -const CALENDAR_END_HOUR = 24; -const PIXELS_PER_MINUTE = CALENDAR_SLOT_HEIGHT / (CALENDAR_SLOT_HOURS * 60); - -function isEventCancelled(event){ - let status = (event.status || "").toLowerCase(); - return status === "cancelled" || status === "canceled" || status === "declined"; -} - -function getEventColorClass(event, currentUserId){ - let isOwnEvent = event.creator_id === currentUserId; - let isPrivateOther = event.visibility === "private" && !isOwnEvent; - - if(isPrivateOther) return "event-private"; - - let statusLower = (event.status || "").toLowerCase(); - - if(statusLower === "confirmed") return "event-confirmed"; - if(statusLower === "busy") return "event-busy"; - - // pending proposals fall back to the same tentative color as busy - return "event-busy"; -} - -function getWeekStart(offset){ - let today = new Date(); - let base = new Date(today.getFullYear(), today.getMonth(), today.getDate() + (offset * 7)); - let dayOfWeek = base.getDay(); - let mondayDiff = (dayOfWeek === 0) ? -6 : (1 - dayOfWeek); - - base.setDate(base.getDate() + mondayDiff); - return base; -} - -function changeCalendarWeek(delta){ - calendarWeekOffset += delta; - - let calendarCard = document.getElementById("calendar-card"); - if(calendarCard){ - calendarCard.innerHTML = buildWeekCalendarHtml(cachedDashboardEvents, calendarWeekOffset); - } -} - -function buildWeekCalendarHtml(events, offset){ - let weekStart = getWeekStart(offset); - let today = new Date(); - let dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; - let days = []; - - for(let i = 0; i < 7; i++){ - let d = new Date(weekStart); - d.setDate(weekStart.getDate() + i); - days.push(d); - } - - let rangeLabel = - days[0].toLocaleDateString("default", { day: "numeric", month: "short" }) - + " - " - + days[6].toLocaleDateString("default", { day: "numeric", month: "short", year: "numeric" }); - - let totalMinutes = (CALENDAR_END_HOUR - CALENDAR_START_HOUR) * 60; - let trackHeight = ((CALENDAR_END_HOUR - CALENDAR_START_HOUR) / CALENDAR_SLOT_HOURS) * CALENDAR_SLOT_HEIGHT; - - let hourRows = ""; - for(let h = CALENDAR_START_HOUR; h < CALENDAR_END_HOUR; h += CALENDAR_SLOT_HOURS){ - hourRows += ` -
- ${String(h).padStart(2, "0")}:00 -
- `; - } - - let dayColumns = days.map(day => { - - let dayEvents = events.filter(event => { - if(!event.start_time || isEventCancelled(event)) return false; - - let eventStart = new Date(event.start_time); - return ( - eventStart.getFullYear() === day.getFullYear() - && eventStart.getMonth() === day.getMonth() - && eventStart.getDate() === day.getDate() - ); - }).sort((a, b) => new Date(a.start_time) - new Date(b.start_time)); - - let layerEnds = []; - - let placed = dayEvents.map(event => { - function parseLocalDate(dateString){ - let parts = dateString.split(/[- :]/); - return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4]); - } - - let start = parseLocalDate(event.start_time); - let end = event.end_time ? parseLocalDate(event.end_time) : new Date(start.getTime() + 30 * 60000); - - let startMinutes = start.getHours() * 60 + start.getMinutes(); - let endMinutes = Math.max(startMinutes + 15, end.getHours() * 60 + end.getMinutes()); - - let layer = 0; - while(layerEnds[layer] !== undefined && layerEnds[layer] > startMinutes){ - layer++; - } - layerEnds[layer] = endMinutes; - - return { event, startMinutes, endMinutes, layer }; - }); - - let maxLayer = placed.reduce((m, p) => Math.max(m, p.layer), 0); - - let blocksHtml = placed.map(p => { - let topPx = (p.startMinutes - CALENDAR_START_HOUR * 60) * PIXELS_PER_MINUTE; - let heightPx = (p.endMinutes - p.startMinutes) * PIXELS_PER_MINUTE; - let leftPct = p.layer * 14; - let widthPct = 100 - (maxLayer * 14); - let colorClass = getEventColorClass(p.event, currentUserId); - - let timeLabel = new Date(p.event.start_time).toLocaleTimeString("nl-NL", { - hour: "2-digit", - minute: "2-digit", - hour12: false - }); - - return ` -
- ${timeLabel} - ${p.event.title} -
- `; - }).join(""); - - let isToday = - day.getFullYear() === today.getFullYear() - && day.getMonth() === today.getMonth() - && day.getDate() === today.getDate(); - - return ` -
-
-
${dayNames[days.indexOf(day)]}
-
${day.getDate()}
-
-
- ${blocksHtml} -
-
- `; - }).join(""); - - return ` -
- -

${rangeLabel}

- -
- -
- confirmed - busy / pending - private (others) -
- -
-
-
-
${hourRows}
-
-
${dayColumns}
-
- `; -} - -// ---------- create / propose event ---------- - -function openCreateEventModal(prefill = {}){ - if(cachedRoomsForForm.length === 0){ - alert("You need to be in a room before you can create an event."); - return; - } - - let kindValue = prefill.kind || "proposal"; - - let roomOptions = cachedRoomsForForm.map(room => ` - - `).join(""); - - openModal(` -

New event

-
- - - - - - - - - - - - -
- -
- -

- - -
- `); - - onEventTypeChange(); -} - -function onEventTypeChange(){ - let kind = document.getElementById("event-type").value; - let wrap = document.getElementById("event-min-people-wrap"); - wrap.style.display = (kind === "proposal") ? "block" : "none"; -} - -async function handleCreateEvent(event){ - event.preventDefault(); - - let kind = document.getElementById("event-type").value; - let roomId = parseInt(document.getElementById("event-room").value, 10); - let title = document.getElementById("event-title").value; - let description = document.getElementById("event-description").value; - let startLocal = document.getElementById("event-start").value; - let endLocal = document.getElementById("event-end").value; - let messageBox = document.getElementById("create-event-message"); - - let payload = { - room_id: roomId, - title, - description, - start_time: toBackendDateTime(startLocal), - end_time: toBackendDateTime(endLocal) - }; - - // Field values follow the convention from the create_event() example - // in events.py: type="proposal", visibility="room", status="proposed". - // "busy" / "private" extend that same pattern for the two other kinds. - if(kind === "proposal"){ - payload.type = "proposal"; - payload.status = "proposed"; - payload.visibility = "room"; - payload.min_people = parseInt(document.getElementById("event-min-people").value, 10) || 1; - } else if(kind === "busy"){ - payload.type = "busy"; - payload.status = "busy"; - payload.visibility = "room"; - payload.min_people = 0; - } else { - payload.type = "private"; - payload.status = "confirmed"; - payload.visibility = "private"; - payload.min_people = 0; - } - - try{ - let result = await apiFetch("/events", { - method: "POST", - body: JSON.stringify(payload) - }); - - // POST /events returns 200 with status:"failed" (not an exception) - // when the room membership check fails. - if(result.status === "failed"){ - messageBox.textContent = - result.reason === "not_room_member" - ? "you're not a member of that room" - : (result.reason || "could not create event"); - return; - } - - closeModal(); - - let activeLink = document.querySelector("nav a.active"); - showPage(activeLink ? activeLink.dataset.page : "dashboard"); - - } catch(error){ - messageBox.textContent = error.message; - } -} - -// ---------- modal infrastructure ---------- - -function ensureModalRoot(){ - let modalRoot = document.getElementById("modal-root"); - - if(!modalRoot){ - modalRoot = document.createElement("div"); - modalRoot.id = "modal-root"; - document.body.appendChild(modalRoot); - } - - return modalRoot; -} - -function openModal(innerHtml){ - let modalRoot = ensureModalRoot(); - - modalRoot.innerHTML = ` - - `; - - document.addEventListener("keydown", handleModalEscape); -} - -function closeModal(){ - let modalRoot = document.getElementById("modal-root"); - if(modalRoot) modalRoot.innerHTML = ""; - - document.removeEventListener("keydown", handleModalEscape); -} - -function handleModalEscape(event){ - if(event.key === "Escape") closeModal(); -} - -// ---------- rooms page ---------- - -async function loadRooms(){ - let content = document.getElementById("content"); - - content.innerHTML = ` -

Rooms

-
Loading...
- `; - - try{ - let roomsData = await apiFetch("/rooms"); - let rooms = roomsData.rooms || []; - cachedRoomsForForm = rooms; - - let actionsHtml = ` -
- -
- -
-

Find a room

-
- - -
-
-
- `; - - if(rooms.length === 0){ - content.innerHTML = ` -

Rooms

- ${actionsHtml} -
Your shared rooms appear here.
- `; - return; - } - - let roomsHtml = rooms.map(room => ` -
-

${room.name}

-

invite code: ${room.invite_code || "-"}

-
- - - -
-
- `).join(""); - - content.innerHTML = ` -

Rooms

- ${actionsHtml} - ${roomsHtml} - `; - - } catch(error){ - content.innerHTML = ` -

Rooms

-
Could not load rooms: ${error.message}
- `; - } -} - -async function handleSearchRoom(event){ - event.preventDefault(); - - let code = document.getElementById("search-room-code").value; - let resultBox = document.getElementById("search-room-result"); - - resultBox.innerHTML = `

Searching...

`; - - try{ - let result = await apiFetch("/search-room", { - method: "POST", - body: JSON.stringify({ invite_code: code }) - }); - - if(result.status === "failed"){ - resultBox.innerHTML = `

No room found for that code.

`; - return; - } - - resultBox.innerHTML = ` -
- ${result.room_name} -
- -
-
- `; - - } catch(error){ - resultBox.innerHTML = `

${error.message}

`; - } -} - -async function handleJoinRoomByCode(code){ - try{ - let result = await apiFetch("/join-room", { - method: "POST", - body: JSON.stringify({ invite_code: code }) - }); - - if(result.status === "failed"){ - alert("Could not join: invalid code, or you're already a member."); - return; - } - - showPage("rooms"); - - } catch(error){ - alert(error.message); - } -} - -async function loadRoomEvents(roomId){ - let content = document.getElementById("content"); - - content.innerHTML = ` -

Room events

-
Loading...
- `; - - try{ - let data = await apiFetch(`/rooms/${roomId}/events`); - let events = data.events || []; - - content.innerHTML = ` -

Room events

-
- -
- ${renderEventCards(events)} - `; - - } catch(error){ - content.innerHTML = ` -

Room events

-
Could not load events: ${error.message}
- `; - } -} - -// ---------- room detail (manage) ---------- - -async function loadRoomDetail(roomId){ - let content = document.getElementById("content"); - - content.innerHTML = `

Room

Loading...
`; - - try{ - let data = await apiFetch(`/rooms/${roomId}`); - let room = data.room; - let members = data.members || []; - let events = data.events || []; - - let ownerUsername = `user #${room.owner_id}`; - try{ - let owner = await apiFetch(`/users/${room.owner_id}`); - ownerUsername = owner.username; - } catch(error){ - // keep the fallback label - } - - content.innerHTML = ` -

${room.name}

- -
-

Owner: ${ownerUsername}

-

Your role: ${room.role}

-
- - -
-
- -
-
-

Members

- -
-
${renderMembersList(members)}
-
- -
-

Leave or delete

-
- - -
-
- `; - - } catch(error){ - content.innerHTML = `

Room

Could not load room: ${error.message}
`; - } -} - -function renderMembersList(members){ - if(!members.length){ - return `

No members found.

`; - } - - return members.map(member => ` -
- ${member.username}${member.role ? ` (${member.role})` : ""} -
- `).join(""); -} - -async function refreshRoomMembers(roomId){ - let listBox = document.getElementById("room-members-list"); - if(!listBox) return; - - listBox.innerHTML = `

Refreshing...

`; - - try{ - let data = await apiFetch(`/rooms/${roomId}/members`); - listBox.innerHTML = renderMembersList(data.members || []); - } catch(error){ - listBox.innerHTML = `

Could not refresh members: ${error.message}

`; - } -} - -async function handleLeaveRoom(roomId){ - if(!confirm("Leave this room?")) return; - - try{ - await apiFetch(`/rooms/${roomId}/members/me`, { method: "DELETE" }); - showPage("rooms"); - } catch(error){ - alert("Could not leave room: " + error.message); - } -} - -async function handleDeleteRoom(roomId){ - if(!confirm("Delete this room? This cannot be undone.")) return; - - try{ - let result = await apiFetch(`/room/${roomId}`, { method: "DELETE" }); - - if(result.status === "denied"){ - alert("You don't have permission to delete this room."); - return; - } - - if(result.status !== "deleted"){ - // the endpoint currently always returns this until it's implemented - alert("Room was not deleted: " + (result.reason || "unknown reason")); - return; - } - - showPage("rooms"); - - } catch(error){ - alert("Could not delete room: " + error.message); - } -} - -// ---------- events page ---------- - -async function loadEvents(){ - let content = document.getElementById("content"); - - content.innerHTML = ` -

Events

-
Loading...
- `; - - try{ - let data = await apiFetch("/events"); - let events = data.events || []; - - content.innerHTML = ` -

Events

-
- -
- ${renderEventCards(events)} - `; - - } catch(error){ - content.innerHTML = ` -

Events

-
Could not load events: ${error.message}
- `; - } -} - -function renderEventCards(events){ - if(events.length === 0){ - return `
No events scheduled.
`; - } - - return events.map(event => ` -
-

${event.title}

-

${event.description || ""}

-

${formatEventTime(event.start_time, event.end_time)}

-

status: ${event.status}

- -
- - - - -
- -

-
- `).join(""); -} - -function formatEventTime(start, end){ - if(!start) return "time unknown"; - - let startDate = new Date(start); - let endDate = end ? new Date(end) : null; - let startText = startDate.toLocaleString(); - - if(!endDate) return startText; - - return startText + " - " + endDate.toLocaleString(); -} - -async function handleRespond(eventId, status){ - let messageBox = document.getElementById(`event-response-${eventId}`); - - try{ - let result = await apiFetch(`/events/${eventId}/respond?status=${status}`, { method: "POST" }); - messageBox.textContent = `you responded: ${status} (${result.going} going)`; - } catch(error){ - messageBox.textContent = error.message; - } -} - -// ---------- event detail / edit / delete ---------- - -async function ensureCurrentUser(){ - if(currentUserId !== null) return currentUserId; - - try{ - let me = await apiFetch("/me"); - currentUserId = me.id; - } catch(error){ - // isOwn checks just stay false - } - - return currentUserId; -} - -async function openEventDetailModal(eventId){ - openModal(`

Loading...

`); - - try{ - await ensureCurrentUser(); - - let eventDetail = await apiFetch(`/events/${eventId}`); - let isOwn = eventDetail.creator_id === currentUserId; - - openModal(` -

${eventDetail.title}

-

${formatEventTime(eventDetail.start_time, eventDetail.end_time)}

-

${eventDetail.description || ""}

-

status: ${eventDetail.status}

- -

Responses

-

Loading responses...

- - - `); - - try{ - let responsesData = await apiFetch(`/events/${eventId}/responses`); - let responses = responsesData.responses || []; - let listBox = document.getElementById("event-responses-list"); - - if(listBox){ - listBox.innerHTML = responses.length - ? responses.map(r => `
${r.username} — ${r.status}
`).join("") - : `

No responses yet.

`; - } - } catch(error){ - let listBox = document.getElementById("event-responses-list"); - if(listBox) listBox.innerHTML = `

Could not load responses: ${error.message}

`; - } - - } catch(error){ - openModal(`

Could not load event: ${error.message}

`); - } -} - -async function openEditEventModal(eventId){ - try{ - let eventDetail = await apiFetch(`/events/${eventId}`); - - openModal(` -

Edit event

-
- - - - - - - - - -

- - -
- `); - - } catch(error){ - alert("Could not load event: " + error.message); - } -} - -async function handleUpdateEvent(event, eventId){ - event.preventDefault(); - - let messageBox = document.getElementById("edit-event-message"); - - let payload = { - title: document.getElementById("edit-event-title").value, - description: document.getElementById("edit-event-description").value, - start_time: toBackendDateTime(document.getElementById("edit-event-start").value), - end_time: toBackendDateTime(document.getElementById("edit-event-end").value), - min_people: parseInt(document.getElementById("edit-event-min-people").value, 10) || 1 - }; - - try{ - await apiFetch(`/events/${eventId}`, { - method: "PATCH", - body: JSON.stringify(payload) - }); - - closeModal(); - showPage("events"); - - } catch(error){ - messageBox.textContent = error.message; - } -} - -async function handleDeleteEvent(eventId){ - if(!confirm("Delete this event?")) return; - - try{ - await apiFetch(`/events/${eventId}`, { method: "DELETE" }); - closeModal(); - showPage("events"); - } catch(error){ - alert("Could not delete event: " + error.message); - } -} - -const A11Y_DEFAULTS = { - reducedMotion: false, - highContrast: false, - fontScale: "normal", - underlineLinks: false, - alwaysShowFocus: false -}; - -function getA11ySettings(){ - try{ - let stored = JSON.parse(localStorage.getItem("a11y-settings")); - return { ...A11Y_DEFAULTS, ...(stored || {}) }; - } catch(error){ - return { ...A11Y_DEFAULTS }; - } -} - -function applyA11ySettings(settings){ - document.body.classList.toggle("reduced-motion", settings.reducedMotion); - document.body.classList.toggle("high-contrast", settings.highContrast); - document.body.classList.toggle("underline-links", settings.underlineLinks); - document.body.classList.toggle("always-focus", settings.alwaysShowFocus); - - document.body.classList.remove("font-scale-large", "font-scale-xlarge"); - if(settings.fontScale === "large") document.body.classList.add("font-scale-large"); - if(settings.fontScale === "xlarge") document.body.classList.add("font-scale-xlarge"); -} - -function updateA11ySetting(key, value){ - let settings = getA11ySettings(); - settings[key] = value; - - localStorage.setItem("a11y-settings", JSON.stringify(settings)); - applyA11ySettings(settings); -} - -function renderSettingsPage(content){ - let settings = getA11ySettings(); - - content.innerHTML = ` -

Settings

- -
-

Account

- -
- -
-

Accessibility

- - - - - - - - - - -
- -
-

Appearance

- -
- -
-

Diagnostics

- -

- - -
- `; -} - -async function runDiagnostics(){ - let resultBox = document.getElementById("diagnostics-result"); - resultBox.textContent = "Checking..."; - - try{ - let home = await apiFetch("/"); - let db = await apiFetch("/database"); - resultBox.textContent = `${home.service}: ${home.status}. Tables: ${(db.tables || []).join(", ")}`; - } catch(error){ - resultBox.textContent = "Diagnostics failed: " + error.message; - } -} - -async function loadAsciiArt(){ - let box = document.getElementById("ascii-art-box"); - - try{ - let headers = {}; - let token = getToken(); - if(token) headers["Authorization"] = "Bearer " + token; - - let response = await fetch(API_BASE + "/ascii-art", { headers }); - let text = await response.text(); - - box.textContent = text; - box.hidden = false; - - } catch(error){ - box.textContent = "Could not load ascii art."; - box.hidden = false; - } -} - -function toggleTheme(){ - document.body.classList.toggle("dark"); - - let theme = document.body.classList.contains("dark") ? "dark" : "light"; - localStorage.setItem("theme", theme); -} - -let savedTheme = localStorage.getItem("theme"); -if(savedTheme === "dark"){ - document.body.classList.add("dark"); -} - -applyA11ySettings(getA11ySettings()); - -if(getToken()){ - showApp(); - showPage("dashboard"); -} else { - showLogin(); -} \ No newline at end of file