deleted vibecoded-ass-website

This commit is contained in:
ben
2026-08-04 15:46:55 +02:00
parent e7e602d321
commit 20996b47bb
2 changed files with 2 additions and 587 deletions
+1
View File
@@ -0,0 +1 @@
hier komt de events pagina
+1 -587
View File
@@ -1,587 +1 @@
<!DOCTYPE html> hier komt de main pagina
<html lang="nl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Calendar</title>
<style>
:root {
--bg: #ffffff;
--fg: #18181b;
--muted: #71717a;
--line: #e4e4e7;
--soft: #f4f4f5;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--fg);
background: var(--bg);
}
h1, h2, h3 { font-weight: 600; margin: 0; }
h1 { font-size: 18px; }
h2 { font-size: 15px; }
input, select, textarea, button { font: inherit; color: inherit; }
input, select, textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--bg);
}
textarea { resize: vertical; }
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
.field { margin-bottom: 12px; }
button {
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--bg);
cursor: pointer;
}
button:hover { background: var(--soft); }
button.primary { background: var(--fg); color: #fff; border-color: var(--fg); }
button.primary:hover { opacity: .9; }
button.link { border: none; background: none; padding: 0; color: var(--muted); text-decoration: underline; }
button:disabled { opacity: .5; cursor: default; }
.row { display: flex; gap: 8px; align-items: center; }
.muted { color: var(--muted); }
.small { font-size: 12px; }
/* auth */
#auth { max-width: 340px; margin: 12vh auto; padding: 0 20px; }
#auth .tabs { display: flex; gap: 16px; margin: 20px 0 16px; }
#auth .tabs button { border: none; background: none; padding: 0 0 6px; border-bottom: 2px solid transparent; }
#auth .tabs button[aria-selected="true"] { border-bottom-color: var(--fg); }
/* app */
#app { display: none; }
header {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 12px 20px; border-bottom: 1px solid var(--line);
}
main { display: flex; align-items: flex-start; }
aside { width: 220px; flex: none; padding: 20px; border-right: 1px solid var(--line); min-height: calc(100vh - 53px); }
section.content { flex: 1; padding: 20px; min-width: 0; }
aside ul { list-style: none; margin: 8px 0 12px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
aside li button { width: 100%; text-align: left; border: none; background: none; border-radius: 6px; padding: 6px 8px; }
aside li button[aria-current="true"] { background: var(--soft); font-weight: 600; }
.day { margin-bottom: 20px; }
.day > h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 8px; }
.event { border: 1px solid var(--line); border-radius: 8px; padding: 12px; margin-bottom: 8px; }
.event .top { display: flex; justify-content: space-between; gap: 12px; }
.event p { margin: 4px 0 0; color: var(--muted); }
.event .actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.event .actions button { padding: 4px 10px; font-size: 12px; }
.event .actions button[aria-pressed="true"] { background: var(--fg); color: #fff; border-color: var(--fg); }
dialog { border: 1px solid var(--line); border-radius: 10px; padding: 20px; width: min(420px, calc(100vw - 32px)); }
dialog::backdrop { background: rgb(0 0 0 / .35); }
dialog h2 { margin-bottom: 16px; }
dialog .cols { display: flex; gap: 8px; }
dialog .cols > * { flex: 1; min-width: 0; }
dialog footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
#toast {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
background: var(--fg); color: #fff; padding: 8px 14px; border-radius: 6px;
font-size: 13px; display: none; max-width: 90vw;
}
@media (max-width: 720px) {
main { flex-direction: column; }
aside { width: 100%; min-height: 0; border-right: none; border-bottom: 1px solid var(--line); }
}
</style>
</head>
<body>
<!-- ---------- auth ---------- -->
<div id="auth">
<h1>Calendar</h1>
<p class="muted small">Room based agenda</p>
<div class="tabs">
<button id="tab-login" aria-selected="true">Inloggen</button>
<button id="tab-register" aria-selected="false">Registreren</button>
</div>
<form id="auth-form">
<div class="field">
<label for="au">Gebruikersnaam</label>
<input id="au" autocomplete="username" required />
</div>
<div class="field">
<label for="ap">Wachtwoord</label>
<input id="ap" type="password" autocomplete="current-password" required />
</div>
<div class="field">
<label for="api">API</label>
<input id="api" />
</div>
<button class="primary" type="submit" id="auth-submit" style="width:100%">Inloggen</button>
</form>
</div>
<!-- ---------- app ---------- -->
<div id="app">
<header>
<div class="row">
<h1>Calendar</h1>
<span class="muted small" id="me"></span>
</div>
<div class="row">
<button id="new-event" class="primary">Nieuw event</button>
<button id="logout" class="link">Uitloggen</button>
</div>
</header>
<main>
<aside>
<h2>Rooms</h2>
<ul id="rooms"></ul>
<div class="row">
<button id="add-room">Nieuw</button>
<button id="join-room">Join</button>
</div>
</aside>
<section class="content">
<div class="row" style="justify-content:space-between;margin-bottom:16px">
<h2 id="list-title">Events</h2>
<div class="row">
<button id="delete-room" class="link" style="display:none">Room verwijderen</button>
<button id="refresh" class="link">Verversen</button>
</div>
</div>
<div id="events"></div>
</section>
</main>
</div>
<!-- ---------- dialogs ---------- -->
<dialog id="dlg-room">
<form id="form-room">
<h2>Nieuwe room</h2>
<div class="field">
<label for="r-name">Naam</label>
<input id="r-name" required />
</div>
<div class="field">
<label for="r-code">Invite code</label>
<input id="r-code" required />
</div>
<footer>
<button type="button" data-close>Annuleren</button>
<button class="primary" type="submit">Aanmaken</button>
</footer>
</form>
</dialog>
<dialog id="dlg-join">
<form id="form-join">
<h2>Room joinen</h2>
<div class="field">
<label for="j-code">Invite code</label>
<input id="j-code" required />
</div>
<p class="small muted" id="j-found"></p>
<footer>
<button type="button" id="j-search">Zoeken</button>
<button type="button" data-close>Annuleren</button>
<button class="primary" type="submit">Joinen</button>
</footer>
</form>
</dialog>
<dialog id="dlg-event">
<form id="form-event">
<h2>Nieuw event</h2>
<div class="field">
<label for="e-title">Titel</label>
<input id="e-title" required />
</div>
<div class="field">
<label for="e-desc">Beschrijving</label>
<textarea id="e-desc" rows="2"></textarea>
</div>
<div class="field cols">
<div>
<label for="e-date">Datum</label>
<input id="e-date" type="date" required />
</div>
<div>
<label for="e-start">Van</label>
<input id="e-start" type="time" value="19:00" required />
</div>
<div>
<label for="e-end">Tot</label>
<input id="e-end" type="time" value="21:00" required />
</div>
</div>
<div class="field cols">
<div>
<label for="e-type">Type</label>
<select id="e-type">
<option value="proposal">proposal</option>
<option value="event">event</option>
</select>
</div>
<div>
<label for="e-status">Status</label>
<select id="e-status">
<option value="proposed">proposed</option>
<option value="confirmed">confirmed</option>
</select>
</div>
<div>
<label for="e-min">Min.</label>
<input id="e-min" type="number" min="1" value="1" />
</div>
</div>
<footer>
<button type="button" data-close>Annuleren</button>
<button class="primary" type="submit">Aanmaken</button>
</footer>
</form>
</dialog>
<div id="toast" role="status" aria-live="polite"></div>
<script>
(() => {
"use strict";
const DEFAULT_API = "http://192.168.254.244:8000";
const LS = { token: "cal_token", api: "cal_api", rooms: "cal_rooms" };
let api = localStorage.getItem(LS.api) || DEFAULT_API;
let token = localStorage.getItem(LS.token) || "";
let userId = null;
let mode = "login";
let rooms = []; // [{ room_id, room_name }]
let activeRoom = null;
let events = [];
const answered = {}; // event_id -> status
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
let toastTimer;
function toast(msg) {
const t = $("toast");
t.textContent = msg;
t.style.display = "block";
clearTimeout(toastTimer);
toastTimer = setTimeout(() => (t.style.display = "none"), 2600);
}
// ---------- api ----------
async function call(path, { method = "GET", body } = {}) {
const res = await fetch(api.replace(/\/+$/, "") + path, {
method,
headers: {
...(body ? { "Content-Type": "application/json" } : {}),
...(token ? { Authorization: "Bearer " + token } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
let data = null;
try { data = await res.json(); } catch {}
if (res.status === 401 || res.status === 403) { logout(); throw new Error("Sessie verlopen"); }
if (!res.ok) {
const d = data && data.detail;
throw new Error(Array.isArray(d) ? (d[0] && d[0].msg) || "Ongeldige invoer" : d || "Er ging iets mis");
}
// the API reports business errors with HTTP 200
if (data && ["failed", "denied", "not_found", "error"].includes(data.status)) {
throw new Error(data.reason || data.status);
}
return data;
}
// ---------- rooms (no "my rooms" endpoint, so cache locally) ----------
function loadRooms() {
try { rooms = JSON.parse(localStorage.getItem(LS.rooms + userId) || "[]"); } catch { rooms = []; }
}
function addRoom(room_id, room_name) {
if (!rooms.some((r) => r.room_id === room_id)) rooms.push({ room_id, room_name });
localStorage.setItem(LS.rooms + userId, JSON.stringify(rooms));
}
// ---------- dates ("2026-08-10 19:00", sometimes malformed) ----------
function parse(str) {
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})[ T](\d{1,2}):(\d{2})/.exec(String(str || ""));
if (!m) return null;
const [, y, mo, d, h, mi] = m.slice(0, 6).map(Number);
if (mo < 1 || mo > 12 || d < 1 || d > 31) return null;
return new Date(y, mo - 1, d, h, mi);
}
const dayKey = (dt) => (dt ? dt.getFullYear() + "-" + String(dt.getMonth() + 1).padStart(2, "0") + "-" + String(dt.getDate()).padStart(2, "0") : "zzzz");
const dayLabel = (dt) => (dt ? dt.toLocaleDateString("nl-NL", { weekday: "long", day: "numeric", month: "long" }) : "Onbekende datum");
const timeLabel = (dt) => (dt ? dt.toLocaleTimeString("nl-NL", { hour: "2-digit", minute: "2-digit" }) : "--:--");
// ---------- auth ----------
function setMode(next) {
mode = next;
$("tab-login").setAttribute("aria-selected", String(next === "login"));
$("tab-register").setAttribute("aria-selected", String(next === "register"));
$("auth-submit").textContent = next === "login" ? "Inloggen" : "Account aanmaken";
$("ap").autocomplete = next === "login" ? "current-password" : "new-password";
}
$("tab-login").onclick = () => setMode("login");
$("tab-register").onclick = () => setMode("register");
$("auth-form").onsubmit = async (e) => {
e.preventDefault();
api = $("api").value.trim() || DEFAULT_API;
localStorage.setItem(LS.api, api);
const body = { username: $("au").value.trim(), password: $("ap").value };
const btn = $("auth-submit");
btn.disabled = true;
try {
if (mode === "register") {
await call("/create-user", { method: "POST", body });
toast("Account aangemaakt, log nu in");
setMode("login");
} else {
const data = await call("/login", { method: "POST", body });
token = data.access_token;
localStorage.setItem(LS.token, token);
await start(body.username);
}
} catch (err) {
toast(err.message);
} finally {
btn.disabled = false;
}
};
function logout() {
token = "";
userId = null;
localStorage.removeItem(LS.token);
$("app").style.display = "none";
$("auth").style.display = "block";
}
$("logout").onclick = logout;
async function start(username) {
const me = await call("/me"); // -> { user_id }
userId = me.user_id;
loadRooms();
$("me").textContent = username ? username + " · #" + userId : "#" + userId;
$("auth").style.display = "none";
$("app").style.display = "block";
await refresh();
}
// ---------- render ----------
function renderRooms() {
$("rooms").innerHTML = rooms.length
? rooms.map((r) => `<li><button data-room="${r.room_id}" aria-current="${r.room_id === activeRoom}">${esc(r.room_name)}</button></li>`).join("")
: '<li class="muted small">Nog geen rooms</li>';
const room = rooms.find((r) => r.room_id === activeRoom);
$("list-title").textContent = room ? room.room_name : "Alle events";
$("delete-room").style.display = room ? "inline" : "none";
}
function renderEvents() {
const list = activeRoom ? events.filter((e) => e.room_id === activeRoom) : events;
if (!list.length) {
$("events").innerHTML = '<p class="muted">Geen events.</p>';
return;
}
const groups = new Map();
for (const ev of list) {
const dt = parse(ev.start_time);
const key = dayKey(dt);
if (!groups.has(key)) groups.set(key, { dt, items: [] });
groups.get(key).items.push(ev);
}
$("events").innerHTML = [...groups.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([, g]) => {
const cards = g.items.map((ev) => {
const btn = (v, label) => `<button data-respond="${ev.id}" data-status="${v}" aria-pressed="${answered[ev.id] === v}">${label}</button>`;
return `
<article class="event">
<div class="top">
<strong>${esc(ev.title)}</strong>
<span class="muted small">${timeLabel(parse(ev.start_time))}&ndash;${timeLabel(parse(ev.end_time))}</span>
</div>
${ev.description ? `<p class="small">${esc(ev.description)}</p>` : ""}
<p class="small">${esc(ev.type)} &middot; ${esc(ev.status)} &middot; min. ${ev.min_people}${ev.going != null ? " &middot; " + ev.going + " gaan" : ""}</p>
<div class="actions">${btn("going", "Ga mee")}${btn("maybe", "Misschien")}${btn("declined", "Kan niet")}</div>
</article>`;
}).join("");
return `<div class="day"><h3>${esc(dayLabel(g.dt))}</h3>${cards}</div>`;
})
.join("");
}
async function refresh() {
try {
const data = await call("/events"); // -> { events: [...] }
events = data.events || [];
for (const ev of events) addRoom(ev.room_id, "Room " + ev.room_id);
if (activeRoom && !rooms.some((r) => r.room_id === activeRoom)) activeRoom = null;
renderRooms();
renderEvents();
} catch (err) {
toast(err.message);
}
}
$("refresh").onclick = refresh;
// ---------- interactions ----------
$("rooms").onclick = (e) => {
const b = e.target.closest("[data-room]");
if (!b) return;
const id = Number(b.dataset.room);
activeRoom = activeRoom === id ? null : id;
renderRooms();
renderEvents();
};
$("events").onclick = async (e) => {
const b = e.target.closest("[data-respond]");
if (!b) return;
const id = Number(b.dataset.respond);
b.disabled = true;
try {
const data = await call(`/events/${id}/respond?status=${encodeURIComponent(b.dataset.status)}`, { method: "POST" });
answered[id] = b.dataset.status;
const ev = events.find((x) => x.id === id);
if (ev && data.going != null) ev.going = data.going;
renderEvents();
} catch (err) {
toast(err.message);
b.disabled = false;
}
};
document.querySelectorAll("[data-close]").forEach((b) => (b.onclick = () => b.closest("dialog").close()));
// create room -> { status:"created", room_id:{ room_id, member_id } }
$("add-room").onclick = () => {
$("r-name").value = "";
$("r-code").value = Math.random().toString(36).slice(2, 8).toUpperCase();
$("dlg-room").showModal();
};
$("form-room").onsubmit = async (e) => {
e.preventDefault();
const name = $("r-name").value.trim();
try {
const data = await call("/create-room", { method: "POST", body: { room_name: name, invite_code: $("r-code").value.trim() } });
const id = data.room_id && data.room_id.room_id;
addRoom(id, name);
activeRoom = id;
$("dlg-room").close();
toast("Room aangemaakt");
renderRooms();
renderEvents();
} catch (err) {
toast(err.message);
}
};
// search + join room (both take { invite_code })
$("join-room").onclick = () => {
$("j-code").value = "";
$("j-found").textContent = "";
$("dlg-join").showModal();
};
$("j-search").onclick = async () => {
try {
const data = await call("/search-room", { method: "POST", body: { invite_code: $("j-code").value.trim() } });
$("j-found").textContent = "Gevonden: " + data.room_name;
} catch {
$("j-found").textContent = "Niet gevonden";
}
};
$("form-join").onsubmit = async (e) => {
e.preventDefault();
const code = $("j-code").value.trim();
try {
const data = await call("/join-room", { method: "POST", body: { invite_code: code } });
let name = "Room " + data.room_id;
try { name = (await call("/search-room", { method: "POST", body: { invite_code: code } })).room_name || name; } catch {}
addRoom(data.room_id, name);
activeRoom = data.room_id;
$("dlg-join").close();
toast("Room gejoind");
await refresh();
} catch (err) {
toast(err.message);
}
};
// delete room
$("delete-room").onclick = async () => {
if (!activeRoom || !confirm("Deze room verwijderen?")) return;
try {
await call("/room/" + activeRoom, { method: "DELETE" });
rooms = rooms.filter((r) => r.room_id !== activeRoom);
localStorage.setItem(LS.rooms + userId, JSON.stringify(rooms));
activeRoom = null;
toast("Room verwijderd");
await refresh();
} catch (err) {
toast(err.message);
}
};
// create event (EventCreate model, naive ISO times)
$("new-event").onclick = () => {
if (!activeRoom) return toast("Kies eerst een room");
$("e-title").value = "";
$("e-desc").value = "";
const n = new Date();
$("e-date").value = n.getFullYear() + "-" + String(n.getMonth() + 1).padStart(2, "0") + "-" + String(n.getDate()).padStart(2, "0");
$("dlg-event").showModal();
};
$("form-event").onsubmit = async (e) => {
e.preventDefault();
const date = $("e-date").value;
try {
await call("/events", {
method: "POST",
body: {
room_id: activeRoom,
type: $("e-type").value,
title: $("e-title").value.trim(),
description: $("e-desc").value.trim(),
start_time: `${date}T${$("e-start").value}:00`,
end_time: `${date}T${$("e-end").value}:00`,
visibility: "room",
status: $("e-status").value,
min_people: Number($("e-min").value) || 1,
},
});
$("dlg-event").close();
toast("Event aangemaakt");
await refresh();
} catch (err) {
toast(err.message);
}
};
// ---------- boot ----------
$("api").value = api;
setMode("login");
if (token) start(null).catch(() => logout());
})();
</script>
</body>
</html>