diff --git a/app.js b/app.js deleted file mode 100644 index c43f528..0000000 --- a/app.js +++ /dev/null @@ -1,1406 +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) - ? (typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail)) - : "request failed"; - throw new Error(message); - } - - return data; -} - - -/* small helpers */ - -function escapeHtml(value){ - return String(value == null ? "" : value) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -function toBackendTime(localValue){ - // datetime-local gives "YYYY-MM-DDTHH:MM" -> "YYYY-MM-DD HH:MM:00" - if(!localValue){ - return null; - } - let value = localValue.replace("T", " "); - if(value.length === 16){ - value += ":00"; - } - return value; -} - - -// ---------- 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.classList.add("success"); - errorBox.classList.remove("error"); - errorBox.textContent = "account created, you can log in now"; - - } catch(error){ - errorBox.classList.remove("success"); - errorBox.classList.add("error"); - errorBox.textContent = error.message; - } -} - - -function logout(){ - clearToken(); - showLogin(); -} - - -// ---------- shared state ---------- - - -let calendarWeekOffset = 0; -let cachedDashboardEvents = []; -let cachedRooms = []; -let currentUserId = null; -let currentUsername = null; - - -async function ensureMe(){ - if(currentUserId !== null){ - return; - } - let me = await apiFetch("/me"); - currentUserId = me.id; - currentUsername = me.username; -} - -async function ensureRooms(force){ - if(cachedRooms.length && !force){ - return cachedRooms; - } - let roomsData = await apiFetch("/rooms"); - cachedRooms = roomsData.rooms || []; - return cachedRooms; -} - - -// ---------- 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"){ loadSettings(); } -} - - -// ---------- dashboard ---------- - - -async function loadDashboard(){ - - let content = document.getElementById("content"); - - content.innerHTML = ` -

Dashboard

-
Loading...
- `; - - calendarWeekOffset = 0; - - try{ - await ensureMe(); - let rooms = await ensureRooms(true); - - let eventsData = await apiFetch("/events"); - cachedDashboardEvents = eventsData.events || []; - - let roomsHtml = - rooms.length - ? rooms.map(room => ` -
${escapeHtml(room.room_name)}
- `).join("") - : `

You have no rooms yet.

`; - - content.innerHTML = ` -

Dashboard

- -
-

Welcome back, ${escapeHtml(currentUsername)}

-

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: ${escapeHtml(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"; - await ensureRooms(true); - 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{ - await apiFetch("/join-room", { - method: "POST", - body: JSON.stringify({ invite_code: inviteCode }) - }); - - messageBox.textContent = "joined room"; - await ensureRooms(true); - 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 status = (event.status || "").toLowerCase(); - let type = (event.type || "").toLowerCase(); - - if(status === "proposed"){ - return "event-proposed"; - } - - if(type === "busy"){ - return "event-busy"; - } - - if(status === "confirmed"){ - return "event-confirmed"; - } - - 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(/[- :T]/); - return new Date( - parts[0], parts[1] - 1, parts[2], - parts[3] || 0, parts[4] || 0 - ); - } - - 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} - ${escapeHtml(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 - proposed - busy - private (others) -
- -
-
-
-
${hourRows}
-
-
${dayColumns}
-
- `; -} - - -// ---------- rooms page ---------- - - -async function loadRooms(){ - - let content = document.getElementById("content"); - - content.innerHTML = ` -

Rooms

-
Loading...
- `; - - try{ - await ensureMe(); - let rooms = await ensureRooms(true); - - let searchCard = ` -
-

Find a room by invite code

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

Rooms

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

${escapeHtml(room.room_name)}

- ${isOwner ? `owner` : ""} -
-

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

-
-
- - - ${isOwner - ? `` - : ``} -
-

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

Rooms

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

Rooms

-
Could not load rooms: ${escapeHtml(error.message)}
- `; - } -} - - -async function handleSearchRoom(event){ - - event.preventDefault(); - - let inviteCode = document.getElementById("search-room-code").value; - let resultBox = document.getElementById("search-room-result"); - - resultBox.textContent = "searching..."; - - try{ - let data = await apiFetch("/search-room", { - method: "POST", - body: JSON.stringify({ invite_code: inviteCode }) - }); - - let room = data.room || data; - - if(!room || (!room.room_name && !room.id)){ - resultBox.textContent = "no room found for that code"; - return; - } - - resultBox.innerHTML = ` -
- ${escapeHtml(room.room_name || "room")} -
- -
-
- `; - - } catch(error){ - resultBox.textContent = error.message; - } -} - - -async function handleJoinRoomByCode(inviteCode){ - try{ - await apiFetch("/join-room", { - method: "POST", - body: JSON.stringify({ invite_code: inviteCode }) - }); - await ensureRooms(true); - loadRooms(); - } catch(error){ - let resultBox = document.getElementById("search-room-result"); - if(resultBox){ resultBox.textContent = error.message; } - } -} - - -async function showRoomMembers(roomId){ - - let container = document.getElementById(`room-members-${roomId}`); - if(!container){ return; } - - container.innerHTML = `loading members...`; - - try{ - let data = await apiFetch(`/rooms/${roomId}/members`); - let members = data.members || []; - - if(members.length === 0){ - container.innerHTML = `no members`; - return; - } - - container.innerHTML = - members.map(m => ` - ${escapeHtml(m.username || ("user " + m.id))} - `).join(""); - - } catch(error){ - container.innerHTML = `${escapeHtml(error.message)}`; - } -} - - -async function handleLeaveRoom(roomId){ - - let messageBox = document.getElementById(`room-message-${roomId}`); - - try{ - await apiFetch(`/rooms/${roomId}/members/me`, { method: "DELETE" }); - await ensureRooms(true); - loadRooms(); - } catch(error){ - if(messageBox){ messageBox.textContent = error.message; } - } -} - - -async function handleDeleteRoom(roomId){ - - let messageBox = document.getElementById(`room-message-${roomId}`); - - try{ - await apiFetch(`/room/${roomId}`, { method: "DELETE" }); - await ensureRooms(true); - loadRooms(); - } catch(error){ - if(messageBox){ messageBox.textContent = error.message; } - } -} - - -async function loadRoomEvents(roomId){ - - let content = document.getElementById("content"); - - content.innerHTML = ` -

Room events

-
Loading...
- `; - - try{ - await ensureMe(); - 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: ${escapeHtml(error.message)}
- `; - } -} - - -// ---------- events page ---------- - - -// kind -> backend fields mapping -const EVENT_KINDS = { - confirmed: { type: "event", visibility: "public", status: "confirmed", label: "Confirmed event", desc: "A regular event everyone can see." }, - proposed: { type: "event", visibility: "public", status: "proposed", label: "Propose event", desc: "Suggest a time and collect responses." }, - busy: { type: "busy", visibility: "public", status: "confirmed", label: "Busy", desc: "Block time without sharing details." }, - private: { type: "private", visibility: "private", status: "confirmed", label: "Private", desc: "Only you can see the details." } -}; - - -async function loadEvents(){ - - let content = document.getElementById("content"); - - content.innerHTML = ` -

Events

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

Events

- ${renderCreateEventCard(rooms)} -

Upcoming events

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

Events

-
Could not load events: ${escapeHtml(error.message)}
- `; - } -} - - -function renderCreateEventCard(rooms){ - - let roomOptions = - rooms.length - ? rooms.map(r => ``).join("") - : ``; - - let kindOptions = - Object.keys(EVENT_KINDS).map((key, index) => { - let k = EVENT_KINDS[key]; - return ` - - `; - }).join(""); - - return ` -
-

Create an event

-
- - -
${kindOptions}
- - - - - - - - - - -
-
- - -
-
- - -
-
- - - - -
-

-
- `; -} - - -function onEventKindChange(){ - - let selected = - document.querySelector('input[name="event-kind"]:checked'); - - let wrap = document.getElementById("event-minpeople-wrap"); - - if(!selected || !wrap){ return; } - - // min people is only meaningful when proposing - wrap.style.display = (selected.value === "proposed") ? "block" : "none"; -} - - -async function handleCreateEvent(event){ - - event.preventDefault(); - - let messageBox = document.getElementById("create-event-message"); - messageBox.classList.remove("success", "error"); - - let kindKey = - (document.querySelector('input[name="event-kind"]:checked') || {}).value || "confirmed"; - - let kind = EVENT_KINDS[kindKey]; - - let roomId = document.getElementById("event-room").value; - let title = document.getElementById("event-title").value; - let description = document.getElementById("event-description").value; - let start = toBackendTime(document.getElementById("event-start").value); - let end = toBackendTime(document.getElementById("event-end").value); - - let minPeople = 0; - if(kindKey === "proposed"){ - minPeople = parseInt(document.getElementById("event-minpeople").value, 10) || 0; - } - - if(!roomId){ - messageBox.classList.add("error"); - messageBox.textContent = "please create or join a room first"; - return; - } - - let payload = { - room_id: parseInt(roomId, 10), - type: kind.type, - title: title, - description: description, - start_time: start, - end_time: end, - visibility: kind.visibility, - status: kind.status, - min_people: minPeople - }; - - try{ - await apiFetch("/events", { - method: "POST", - body: JSON.stringify(payload) - }); - - messageBox.classList.add("success"); - messageBox.textContent = `${kind.label} created`; - - // refresh dashboard cache and reload the events list - loadEvents(); - - } catch(error){ - messageBox.classList.add("error"); - messageBox.textContent = error.message; - } -} - - -function eventBadge(event){ - - let status = (event.status || "").toLowerCase(); - let type = (event.type || "").toLowerCase(); - - if(event.visibility === "private"){ - return `private`; - } - if(status === "proposed"){ - return `proposed`; - } - if(type === "busy"){ - return `busy`; - } - return `confirmed`; -} - - -function renderEventCards(events){ - - if(events.length === 0){ - return `
No events scheduled.
`; - } - - return events.map(event => { - - let isOwner = event.creator_id === currentUserId; - let isProposed = (event.status || "").toLowerCase() === "proposed"; - - return ` -
- -
-

${escapeHtml(event.title)}

- ${eventBadge(event)} -
- -

${escapeHtml(event.description || "")}

- -

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

- - ${isProposed && event.min_people - ? `

needs at least ${event.min_people} people

` - : ""} - -
- - - - - ${isOwner ? `` : ""} - ${isOwner ? `` : ""} -
- -

-
-
- -
- `; - }).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" }); - - let going = (result && result.going != null) ? result.going : "?"; - messageBox.textContent = `you responded: ${status} (${going} going)`; - - } catch(error){ - messageBox.textContent = error.message; - } -} - - -async function viewResponses(eventId){ - - let container = document.getElementById(`event-responses-${eventId}`); - if(!container){ return; } - - container.innerHTML = `

loading responses...

`; - - try{ - let data = await apiFetch(`/events/${eventId}/responses`); - let responses = data.responses || []; - - if(responses.length === 0){ - container.innerHTML = `

no responses yet

`; - return; - } - - container.innerHTML = ` -
- ${responses.map(r => ` - - ${escapeHtml(r.username || ("user " + r.user_id))}: ${escapeHtml(r.status)} - - `).join("")} -
- `; - - } catch(error){ - container.innerHTML = `

${escapeHtml(error.message)}

`; - } -} - - -async function toggleEditEvent(eventId){ - - let container = document.getElementById(`event-edit-${eventId}`); - if(!container){ return; } - - if(container.innerHTML.trim() !== ""){ - container.innerHTML = ""; - return; - } - - try{ - let event = await apiFetch(`/events/${eventId}`); - let ev = event.event || event; - - let startVal = (ev.start_time || "").replace(" ", "T").slice(0, 16); - let endVal = (ev.end_time || "").replace(" ", "T").slice(0, 16); - - container.innerHTML = ` -
- - - - - - -
-
- - -
-
- - -
-
- - - - - -
-

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

${escapeHtml(error.message)}

`; - } -} - - -async function handleUpdateEvent(event, eventId){ - - event.preventDefault(); - - let messageBox = document.getElementById(`edit-message-${eventId}`); - - let payload = { - title: document.getElementById(`edit-title-${eventId}`).value, - description: document.getElementById(`edit-desc-${eventId}`).value, - start_time: toBackendTime(document.getElementById(`edit-start-${eventId}`).value), - end_time: toBackendTime(document.getElementById(`edit-end-${eventId}`).value), - min_people: parseInt(document.getElementById(`edit-minpeople-${eventId}`).value, 10) || 0 - }; - - try{ - await apiFetch(`/events/${eventId}`, { - method: "PATCH", - body: JSON.stringify(payload) - }); - - messageBox.classList.add("success"); - messageBox.textContent = "event updated"; - loadEvents(); - - } catch(error){ - messageBox.textContent = error.message; - } -} - - -async function handleDeleteEvent(eventId){ - - try{ - await apiFetch(`/events/${eventId}`, { method: "DELETE" }); - - let card = document.getElementById(`event-card-${eventId}`); - if(card){ card.remove(); } - - } catch(error){ - let messageBox = document.getElementById(`event-response-${eventId}`); - if(messageBox){ messageBox.textContent = error.message; } - } -} - - -// ---------- settings page ---------- - - -const A11Y_DEFAULTS = { - contrast: false, - underline: false, - reduceMotion: false, - dyslexic: false, - fontScale: "1" -}; - - -function getA11ySettings(){ - try{ - let raw = localStorage.getItem("a11y"); - if(!raw){ return { ...A11Y_DEFAULTS }; } - return { ...A11Y_DEFAULTS, ...JSON.parse(raw) }; - } catch(e){ - return { ...A11Y_DEFAULTS }; - } -} - - -function saveA11ySettings(settings){ - localStorage.setItem("a11y", JSON.stringify(settings)); -} - - -function applyA11ySettings(){ - - let s = getA11ySettings(); - let body = document.body; - - body.classList.toggle("a11y-contrast", !!s.contrast); - body.classList.toggle("a11y-underline", !!s.underline); - body.classList.toggle("a11y-reduce-motion", !!s.reduceMotion); - body.classList.toggle("a11y-dyslexic", !!s.dyslexic); - - document.documentElement.style.setProperty("--font-scale", s.fontScale || "1"); -} - - -function updateA11ySetting(key, value){ - let s = getA11ySettings(); - s[key] = value; - saveA11ySettings(s); - applyA11ySettings(); -} - - -async function loadSettings(){ - - let content = document.getElementById("content"); - let s = getA11ySettings(); - - let isDark = document.body.classList.contains("dark"); - - content.innerHTML = ` -

Settings

- -
-

Appearance

- -
-
-
Dark mode
-
Switch between the light paper theme and the dark theme.
-
-
- -
-
- -
-
-
Text size
-
Scale all text for easier reading.
-
-
- - -
-
-
- -
-

Accessibility

- -
-
-
High contrast
-
Increase contrast and thicken borders for better visibility.
-
-
- -
-
- -
-
-
Underline links
-
Always underline links so they stand out from regular text.
-
-
- -
-
- -
-
-
Reduce motion
-
Turn off transitions and animations across the app.
-
-
- -
-
- -
-
-
Dyslexia-friendly reading
-
Use a more legible font with extra letter and line spacing.
-
-
- -
-
- -
- -
-
- -
-

Account

-

Loading account...

- -
- `; - - // load account info via /me - try{ - await ensureMe(); - let box = document.getElementById("settings-account"); - if(box){ - box.textContent = `Signed in as ${currentUsername} (id ${currentUserId})`; - } - } catch(e){ /* ignore */ } -} - - -function resetA11ySettings(){ - saveA11ySettings({ ...A11Y_DEFAULTS }); - applyA11ySettings(); - loadSettings(); -} - - -// ---------- theme ---------- - - -function toggleTheme(){ - - document.body.classList.toggle("dark"); - - let theme = - document.body.classList.contains("dark") ? "dark" : "light"; - - localStorage.setItem("theme", theme); -} - - -// ---------- init ---------- - - -let savedTheme = localStorage.getItem("theme"); - -if(savedTheme === "dark"){ - document.body.classList.add("dark"); -} - -applyA11ySettings(); - -if(getToken()){ - showApp(); - showPage("dashboard"); -} else { - showLogin(); -}