const API_BASE = "http://localhost:8000"; 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; } // ---------- 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"){ content.innerHTML = `

Settings

Account preferences.

`; } } // ---------- dashboard ---------- let calendarWeekOffset = 0; let cachedDashboardEvents = []; 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 || []; let eventsData = await apiFetch("/events"); cachedDashboardEvents = eventsData.events || []; let roomsHtml = rooms.length ? rooms.map(room => `
${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{ await apiFetch( "/join-room", { method: "POST", body: JSON.stringify({ invite_code: inviteCode }) } ); 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"; } if((event.status || "").toLowerCase() === "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; // time gutter let hourRows = ""; 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); 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(/[- :]/); 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 heightPct = ((p.endMinutes - p.startMinutes) / totalMinutes) * 100; 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 }); console.log({ title: p.event.title, startMinutes: p.startMinutes, endMinutes: p.endMinutes, topPx, heightPx }); 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 private (others)
${hourRows}
${dayColumns}
`; } // ---------- rooms page ---------- async function loadRooms(){ let content = document.getElementById("content"); content.innerHTML = `

Rooms

Loading...
`; try{ let roomsData = await apiFetch("/rooms"); let rooms = roomsData.rooms || []; if(rooms.length === 0){ content.innerHTML = `

Rooms

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

${room.room_name}

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

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

Rooms

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

Rooms

Could not load rooms: ${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}
`; } } // ---------- 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; } } // ---------- 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"); } if(getToken()){ showApp(); showPage("dashboard"); } else { showLogin(); }