diff --git a/backend/main.py b/backend/main.py
index c922ccd..9daab64 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -14,7 +14,7 @@ import sqlite3
from typing import Literal
from fastapi.middleware.cors import CORSMiddleware
-from events import delete_event
+from events import delete_event, get_room_events
class Login(BaseModel):
username: str
diff --git a/frontend/www/app.js b/frontend/www/app.js
new file mode 100644
index 0000000..12de3fa
--- /dev/null
+++ b/frontend/www/app.js
@@ -0,0 +1,1144 @@
+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
+
+
+
+
+
+
+
+ `;
+
+ } 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_HOUR_HEIGHT = 48;
+
+const CALENDAR_START_HOUR = 0;
+
+const CALENDAR_END_HOUR = 24;
+
+
+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_HOUR_HEIGHT;
+
+
+ // time gutter
+
+ let hourRows = "";
+
+
+ for(let h = CALENDAR_START_HOUR; h < CALENDAR_END_HOUR; h++){
+
+ 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 => {
+
+ let start =
+ new Date(event.start_time);
+
+ let end =
+ event.end_time
+ ? new Date(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 topPct =
+ (p.startMinutes / totalMinutes) * 100;
+
+ 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("default", { hour: "2-digit", minute: "2-digit" });
+
+
+ return `
+
+ ${timeLabel}
+ ${p.event.title}
+
+ `;
+
+ }).join("");
+
+
+ let isToday =
+ (
+ day.getFullYear() === today.getFullYear()
+ && day.getMonth() === today.getMonth()
+ && day.getDate() === today.getDate()
+ );
+
+
+ return `
+
+
+
+
+
+ ${blocksHtml}
+
+
+
+ `;
+
+ }).join("");
+
+
+ return `
+
+
+
+
+
+ confirmed
+
+
+
+ busy
+
+
+
+ private (others)
+
+
+
+
+
+
+
+
+
+ ${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();
+
+}
\ No newline at end of file
diff --git a/frontend/www/events/index.html b/frontend/www/events/index.html
deleted file mode 100644
index 8d4ad6d..0000000
--- a/frontend/www/events/index.html
+++ /dev/null
@@ -1 +0,0 @@
-hier komt de events pagina
\ No newline at end of file
diff --git a/frontend/www/index.html b/frontend/www/index.html
index 46effad..b4f3f5b 100644
--- a/frontend/www/index.html
+++ b/frontend/www/index.html
@@ -1,26 +1,100 @@
- Calendar
+ Calendar Dashboard
+
-
-