deleted vibecoded-ass-website
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
hier komt de events pagina
|
||||||
+1
-587
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[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))}–${timeLabel(parse(ev.end_time))}</span>
|
|
||||||
</div>
|
|
||||||
${ev.description ? `<p class="small">${esc(ev.description)}</p>` : ""}
|
|
||||||
<p class="small">${esc(ev.type)} · ${esc(ev.status)} · min. ${ev.min_people}${ev.going != null ? " · " + 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>
|
|
||||||
Reference in New Issue
Block a user