1178 lines
22 KiB
JavaScript
1178 lines
22 KiB
JavaScript
const API_BASE =
|
|
"/api";
|
|
|
|
|
|
|
|
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 = `
|
|
<h1>Settings</h1>
|
|
|
|
<div class="card">
|
|
Account preferences.
|
|
<br><br>
|
|
<button onclick="logout()">Log out</button>
|
|
</div>
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------- dashboard ----------
|
|
|
|
|
|
let calendarWeekOffset = 0;
|
|
|
|
let cachedDashboardEvents = [];
|
|
|
|
let currentUserId = null;
|
|
|
|
|
|
async function loadDashboard(){
|
|
|
|
let content =
|
|
document.getElementById("content");
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Dashboard</h1>
|
|
<div class="card">Loading...</div>
|
|
`;
|
|
|
|
|
|
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 => `
|
|
<div class="list-item">
|
|
${room.room_name}
|
|
</div>
|
|
`).join("")
|
|
: `<p class="muted">You have no rooms yet.</p>`;
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Dashboard</h1>
|
|
|
|
<div class="card">
|
|
<h3>Welcome back, ${me.username}</h3>
|
|
<p>
|
|
Overview of your calendar.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card" id="calendar-card">
|
|
${buildWeekCalendarHtml(cachedDashboardEvents, calendarWeekOffset)}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Your rooms</h3>
|
|
${roomsHtml}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Create a room</h3>
|
|
|
|
<form onsubmit="handleCreateRoom(event)">
|
|
|
|
<input
|
|
type="text"
|
|
id="create-room-name"
|
|
placeholder="Room name"
|
|
required
|
|
>
|
|
|
|
<input
|
|
type="text"
|
|
id="create-room-code"
|
|
placeholder="Invite code"
|
|
required
|
|
>
|
|
|
|
<button type="submit">Create room</button>
|
|
|
|
</form>
|
|
|
|
<p id="create-room-message" class="muted"></p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Join a room</h3>
|
|
|
|
<form onsubmit="handleJoinRoom(event)">
|
|
|
|
<input
|
|
type="text"
|
|
id="join-room-code"
|
|
placeholder="Invite code"
|
|
required
|
|
>
|
|
|
|
<button type="submit">Join room</button>
|
|
|
|
</form>
|
|
|
|
<p id="join-room-message" class="muted"></p>
|
|
</div>
|
|
`;
|
|
|
|
} catch(error){
|
|
|
|
content.innerHTML = `
|
|
<h1>Dashboard</h1>
|
|
<div class="card">Could not load dashboard: ${error.message}</div>
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
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 += `
|
|
<div
|
|
class="calendar-hour-label"
|
|
style="height:${CALENDAR_SLOT_HEIGHT}px"
|
|
>
|
|
${String(h).padStart(2, "0")}:00
|
|
</div>
|
|
`;
|
|
|
|
}
|
|
|
|
// 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 `
|
|
<div
|
|
class="calendar-event ${colorClass}"
|
|
style="
|
|
top:${topPx}px;
|
|
height:${heightPx}px;
|
|
left:${leftPct}%;
|
|
width:${widthPct}%;
|
|
z-index:${p.layer + 1};
|
|
"
|
|
onclick="showPage('events')"
|
|
title="${p.event.title}"
|
|
>
|
|
<span class="calendar-event-time">${timeLabel}</span>
|
|
<span class="calendar-event-title">${p.event.title}</span>
|
|
</div>
|
|
`;
|
|
|
|
}).join("");
|
|
|
|
|
|
let isToday =
|
|
(
|
|
day.getFullYear() === today.getFullYear()
|
|
&& day.getMonth() === today.getMonth()
|
|
&& day.getDate() === today.getDate()
|
|
);
|
|
|
|
|
|
return `
|
|
<div class="calendar-day-column">
|
|
|
|
<div class="calendar-day-header ${isToday ? "today" : ""}">
|
|
<div class="calendar-day-name">${dayNames[days.indexOf(day)]}</div>
|
|
<div class="calendar-day-number">${day.getDate()}</div>
|
|
</div>
|
|
|
|
<div class="calendar-day-track" style="height:${trackHeight}px">
|
|
${blocksHtml}
|
|
</div>
|
|
|
|
</div>
|
|
`;
|
|
|
|
}).join("");
|
|
|
|
|
|
return `
|
|
<div class="calendar-header">
|
|
|
|
<button class="calendar-nav-btn" onclick="changeCalendarWeek(-1)">
|
|
<
|
|
</button>
|
|
|
|
<h3>${rangeLabel}</h3>
|
|
|
|
<button class="calendar-nav-btn" onclick="changeCalendarWeek(1)">
|
|
>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div class="calendar-legend">
|
|
|
|
<span class="legend-item">
|
|
<span class="legend-dot event-confirmed"></span> confirmed
|
|
</span>
|
|
|
|
<span class="legend-item">
|
|
<span class="legend-dot event-busy"></span> busy
|
|
</span>
|
|
|
|
<span class="legend-item">
|
|
<span class="legend-dot event-private"></span> private (others)
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<div class="calendar-week">
|
|
|
|
<div class="calendar-gutter">
|
|
<div class="calendar-day-header"></div>
|
|
<div class="calendar-hour-labels">
|
|
${hourRows}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="calendar-days">
|
|
${dayColumns}
|
|
</div>
|
|
|
|
</div>
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------- rooms page ----------
|
|
|
|
|
|
async function loadRooms(){
|
|
|
|
let content =
|
|
document.getElementById("content");
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Rooms</h1>
|
|
<div class="card">Loading...</div>
|
|
`;
|
|
|
|
|
|
try{
|
|
|
|
let roomsData =
|
|
await apiFetch("/rooms");
|
|
|
|
let rooms =
|
|
roomsData.rooms || [];
|
|
|
|
|
|
if(rooms.length === 0){
|
|
|
|
content.innerHTML = `
|
|
<h1>Rooms</h1>
|
|
<div class="card">
|
|
Your shared rooms appear here.
|
|
</div>
|
|
`;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
let roomsHtml =
|
|
rooms.map(room => `
|
|
<div class="card">
|
|
<h3>${room.room_name}</h3>
|
|
<p class="muted">invite code: ${room.invite_code || "-"}</p>
|
|
<button onclick="loadRoomEvents(${room.id})">
|
|
View events
|
|
</button>
|
|
</div>
|
|
`).join("");
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Rooms</h1>
|
|
${roomsHtml}
|
|
`;
|
|
|
|
} catch(error){
|
|
|
|
content.innerHTML = `
|
|
<h1>Rooms</h1>
|
|
<div class="card">Could not load rooms: ${error.message}</div>
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
async function loadRoomEvents(roomId){
|
|
|
|
let content =
|
|
document.getElementById("content");
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Room events</h1>
|
|
<div class="card">Loading...</div>
|
|
`;
|
|
|
|
|
|
try{
|
|
|
|
let data =
|
|
await apiFetch(`/rooms/${roomId}/events`);
|
|
|
|
let events =
|
|
data.events || [];
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Room events</h1>
|
|
${renderEventCards(events)}
|
|
`;
|
|
|
|
} catch(error){
|
|
|
|
content.innerHTML = `
|
|
<h1>Room events</h1>
|
|
<div class="card">Could not load events: ${error.message}</div>
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------- events page ----------
|
|
|
|
|
|
async function loadEvents(){
|
|
|
|
let content =
|
|
document.getElementById("content");
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Events</h1>
|
|
<div class="card">Loading...</div>
|
|
`;
|
|
|
|
|
|
try{
|
|
|
|
let data =
|
|
await apiFetch("/events");
|
|
|
|
let events =
|
|
data.events || [];
|
|
|
|
|
|
content.innerHTML = `
|
|
<h1>Events</h1>
|
|
${renderEventCards(events)}
|
|
`;
|
|
|
|
} catch(error){
|
|
|
|
content.innerHTML = `
|
|
<h1>Events</h1>
|
|
<div class="card">Could not load events: ${error.message}</div>
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
function renderEventCards(events){
|
|
|
|
if(events.length === 0){
|
|
|
|
return `<div class="card">No events scheduled.</div>`;
|
|
|
|
}
|
|
|
|
|
|
return events.map(event => `
|
|
|
|
<div class="card">
|
|
|
|
<h3>${event.title}</h3>
|
|
|
|
<p>${event.description || ""}</p>
|
|
|
|
<p class="muted">
|
|
${formatEventTime(event.start_time, event.end_time)}
|
|
</p>
|
|
|
|
<p class="muted">
|
|
status: ${event.status}
|
|
</p>
|
|
|
|
<div class="event-actions">
|
|
|
|
<button onclick="handleRespond(${event.id}, 'going')">
|
|
Going
|
|
</button>
|
|
|
|
<button onclick="handleRespond(${event.id}, 'maybe')">
|
|
Maybe
|
|
</button>
|
|
|
|
<button onclick="handleRespond(${event.id}, 'declined')">
|
|
Declined
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<p
|
|
id="event-response-${event.id}"
|
|
class="muted"
|
|
></p>
|
|
|
|
</div>
|
|
|
|
`).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();
|
|
|
|
} |