`;
-
}
-
}
@@ -413,41 +294,23 @@ 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");
-
+ 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
- })
- }
- );
-
+ 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;
-
}
-
}
@@ -455,116 +318,90 @@ async function handleJoinRoom(event){
event.preventDefault();
-
- let inviteCode =
- document.getElementById("join-room-code").value;
-
- let messageBox =
- document.getElementById("join-room-message");
-
+ 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 })
- }
- );
-
+ 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();
-
+ 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;
-
+ 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((event.status || "").toLowerCase() === "confirmed"){
+ 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 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);
+ 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;
-
}
@@ -572,561 +409,969 @@ function changeCalendarWeek(delta){
calendarWeekOffset += delta;
-
- let calendarCard =
- document.getElementById("calendar-card");
+ 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 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);
-
+ 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 totalMinutes = (CALENDAR_END_HOUR - CALENDAR_START_HOUR) * 60;
let trackHeight =
((CALENDAR_END_HOUR - CALENDAR_START_HOUR) / CALENDAR_SLOT_HOURS)
* CALENDAR_SLOT_HEIGHT;
- // time gutter
-
let hourRows = "";
-
- for(
- let h = CALENDAR_START_HOUR;
- h < CALENDAR_END_HOUR;
- h += CALENDAR_SLOT_HOURS
- ){
-
+ for(let h = CALENDAR_START_HOUR; h < CALENDAR_END_HOUR; h += CALENDAR_SLOT_HOURS){
hourRows += `
-
+
${String(h).padStart(2, "0")}:00
`;
-
}
- // day columns
-
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);
-
+ 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));
-
- // assign overlap layers: an event gets the lowest layer
- // whose most recent block has already ended
-
let layerEnds = [];
let placed =
dayEvents.map(event => {
- function parseLocalDate(dateString){
- let parts = dateString.split(/[- :]/);
+ function parseLocalDate(dateString){
+ let parts = dateString.split(/[- :T]/);
return new Date(
- parts[0],
- parts[1] - 1,
- parts[2],
- parts[3],
- parts[4]
+ 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 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 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 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 heightPx =
- (p.endMinutes - p.startMinutes)
- * PIXELS_PER_MINUTE;
-
- let heightPct =
- ((p.endMinutes - p.startMinutes) / totalMinutes) * 100;
-
- let leftPct =
- p.layer * 14;
-
- let widthPct =
- 100 - (maxLayer * 14);
-
- let colorClass =
- getEventColorClass(p.event, currentUserId);
+ 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
+ hour: "2-digit", minute: "2-digit", hour12: false
});
-
- console.log({
- title: p.event.title,
- startMinutes: p.startMinutes,
- endMinutes: p.endMinutes,
- topPx,
- heightPx
- });
+
return `
${timeLabel}
- ${p.event.title}
+ ${escapeHtml(p.event.title)}
`;
-
}).join("");
-
let isToday =
- (
- day.getFullYear() === today.getFullYear()
- && day.getMonth() === today.getMonth()
- && day.getDate() === today.getDate()
- );
-
+ day.getFullYear() === today.getFullYear()
+ && day.getMonth() === today.getMonth()
+ && day.getDate() === today.getDate();
return `
`;
-
}).join("");
-
return `
-
-
- confirmed
-
-
-
- busy
-
-
-
- private (others)
-
-
+ confirmed
+ proposed
+ busy
+ private (others)
-
-
- ${hourRows}
-
+
${hourRows}
-
-
- ${dayColumns}
-
-
+
${dayColumns}
`;
-
}
-
// ---------- rooms page ----------
async function loadRooms(){
- let content =
- document.getElementById("content");
-
+ let content = document.getElementById("content");
content.innerHTML = `
Rooms
Loading...
`;
-
try{
+ await ensureMe();
+ let rooms = await ensureRooms(true);
- let roomsData =
- await apiFetch("/rooms");
-
- let rooms =
- roomsData.rooms || [];
-
+ let searchCard = `
+
+
Find a room by invite code
+
+
+
+ `;
if(rooms.length === 0){
-
content.innerHTML = `
Rooms
-
- Your shared rooms appear here.
-
+ ${searchCard}
+
Your shared rooms appear here.
`;
-
return;
-
}
-
let roomsHtml =
- rooms.map(room => `
+ rooms.map(room => {
+ let isOwner = room.owner_id === currentUserId;
+ return `
-
${room.room_name}
-
invite code: ${room.invite_code || "-"}
-
+
+
${escapeHtml(room.room_name)}
+ ${isOwner ? `owner` : ""}
+
+
invite code: ${escapeHtml(room.invite_code || "-")}
+
+
+
+
+ ${isOwner
+ ? ``
+ : ``}
+
+
- `).join("");
-
+ `;
+ }).join("");
content.innerHTML = `
Rooms
+ ${searchCard}
${roomsHtml}
`;
} catch(error){
-
content.innerHTML = `
Rooms
-
Could not load rooms: ${error.message}
+
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");
-
+ let content = document.getElementById("content");
content.innerHTML = `
Room events
Loading...
`;
-
try{
-
- let data =
- await apiFetch(`/rooms/${roomId}/events`);
-
- let events =
- data.events || [];
-
+ 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: ${error.message}
+
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");
-
+ 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 || [];
-
+ 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: ${error.message}
+
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 `
+
+ `;
+}
+
+
+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 => {
- return events.map(event => `
+ let isOwner = event.creator_id === currentUserId;
+ let isProposed = (event.status || "").toLowerCase() === "proposed";
-
-
-
${event.title}
-
-
${event.description || ""}
-
-
- ${formatEventTime(event.start_time, event.end_time)}
-
-
-
- status: ${event.status}
-
-
-
-
-
-
-
-
-
+ 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("");
-
+ `;
+ }).join("");
}
function formatEventTime(start, end){
if(!start){
-
return "time unknown";
-
}
+ let startDate = new Date(start);
+ let endDate = end ? new Date(end) : null;
- let startDate =
- new Date(start);
-
- let endDate =
- end
- ? new Date(end)
- : null;
-
-
- let startText =
- startDate.toLocaleString();
+ let startText = startDate.toLocaleString();
if(!endDate){
-
return startText;
-
}
-
return startText + " - " + endDate.toLocaleString();
-
}
async function handleRespond(eventId, status){
- let messageBox =
- document.getElementById(`event-response-${eventId}`);
-
+ let messageBox = document.getElementById(`event-response-${eventId}`);
try{
-
let result =
- await apiFetch(
- `/events/${eventId}/respond?status=${status}`,
- { method: "POST" }
- );
+ await apiFetch(`/events/${eventId}/respond?status=${status}`, { method: "POST" });
-
- messageBox.textContent =
- `you responded: ${status} (${result.going} going)`;
+ 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 ----------
@@ -1135,44 +1380,27 @@ function toggleTheme(){
document.body.classList.toggle("dark");
-
let theme =
- document.body.classList.contains("dark")
- ? "dark"
- : "light";
-
-
- localStorage.setItem(
- "theme",
- theme
- );
+ document.body.classList.contains("dark") ? "dark" : "light";
+ localStorage.setItem("theme", theme);
}
-
// ---------- init ----------
-let savedTheme =
- localStorage.getItem("theme");
-
+let savedTheme = localStorage.getItem("theme");
if(savedTheme === "dark"){
-
document.body.classList.add("dark");
-
}
+applyA11ySettings();
if(getToken()){
-
showApp();
-
showPage("dashboard");
-
} else {
-
showLogin();
-
}
diff --git a/index.html b/index.html
index b4f3f5b..6825d6b 100644
--- a/index.html
+++ b/index.html
@@ -1,12 +1,16 @@
-
+
+
+
Calendar Dashboard
+
Skip to main content
+
-