new frontend

ik ben zo cool
This commit is contained in:
ben
2026-08-19 09:33:48 +02:00
parent 1822dea7e3
commit bf601846db
104 changed files with 16741 additions and 0 deletions
@@ -0,0 +1,205 @@
"use client"
import { useState } from "react"
import useSWR from "swr"
import {
Clock,
Calendar as CalendarIcon,
DoorClosed,
Users,
Check,
X,
HelpCircle,
Trash2,
CheckCircle2,
Ban,
Loader2,
} from "lucide-react"
import { GlassModal } from "./glass-modal"
import * as api from "@/lib/api"
import { ApiError } from "@/lib/api"
import type { CalendarEvent, Room, SignupStatus } from "@/lib/types"
import { getRoomColor } from "@/lib/room-theme"
import { parseDateTime, formatDayLabel, formatTime } from "@/lib/date-utils"
interface EventDetailDialogProps {
event: CalendarEvent
room?: Room
currentUserId: number
onClose: () => void
onChanged: () => void
}
const STATUS_STYLES: Record<string, string> = {
confirmed: "bg-emerald-500/30 text-emerald-100 border-emerald-400/40",
proposed: "bg-amber-500/30 text-amber-100 border-amber-400/40",
draft: "bg-white/20 text-white border-white/30",
cancelled: "bg-red-500/30 text-red-100 border-red-400/40",
}
export function EventDetailDialog({
event,
room,
currentUserId,
onClose,
onChanged,
}: EventDetailDialogProps) {
const [busy, setBusy] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const { data: responses, mutate: mutateResponses } = useSWR(
["responses", event.id],
() => api.getEventResponses(event.id),
)
const start = parseDateTime(event.start_time)
const end = parseDateTime(event.end_time)
const color = getRoomColor(event.room_id)
const canManage =
event.creator_id === currentUserId || room?.role === "admin" || room?.role === "owner"
const myResponse = responses?.find((r) => r.user_id === currentUserId)?.status
const goingCount = responses?.filter((r) => r.status === "going").length ?? 0
const maybeCount = responses?.filter((r) => r.status === "maybe").length ?? 0
const declinedCount = responses?.filter((r) => r.status === "declined").length ?? 0
const run = async (key: string, fn: () => Promise<unknown>, refetchResponses = false) => {
setBusy(key)
setError(null)
try {
await fn()
if (refetchResponses) await mutateResponses()
onChanged()
} catch (err) {
setError(err instanceof ApiError ? err.message : "Action failed.")
} finally {
setBusy(null)
}
}
const respond = (status: SignupStatus) =>
run(`respond-${status}`, () => api.respondToEvent(event.id, status), true)
const rsvpButtons: { status: SignupStatus; label: string; icon: typeof Check }[] = [
{ status: "going", label: "Going", icon: Check },
{ status: "maybe", label: "Maybe", icon: HelpCircle },
{ status: "declined", label: "Can't go", icon: X },
]
return (
<GlassModal title={event.title} onClose={onClose}>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
<span className={`h-3 w-3 rounded-sm ${color.dot}`} />
<span className="text-sm text-white/80">{room?.name ?? `Room #${event.room_id}`}</span>
<span
className={`ml-auto rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${
STATUS_STYLES[event.status] ?? STATUS_STYLES.draft
}`}
>
{event.status}
</span>
</div>
<div className="space-y-2 text-sm text-white/90">
<p className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4 text-white/70" />
{formatDayLabel(start)}
</p>
<p className="flex items-center gap-2">
<Clock className="h-4 w-4 text-white/70" />
{formatTime(start)} {formatTime(end)}
</p>
<p className="flex items-center gap-2 capitalize">
<DoorClosed className="h-4 w-4 text-white/70" />
{event.type} · {event.visibility.replace("_", " ")}
</p>
{event.type === "proposal" && (
<p className="flex items-center gap-2">
<Users className="h-4 w-4 text-white/70" />
{goingCount} going · needs {event.min_people}
</p>
)}
</div>
{event.description && (
<p className="rounded-xl border border-white/15 bg-white/5 p-3 text-sm text-white/85">
{event.description}
</p>
)}
{/* RSVP */}
{event.status !== "cancelled" && (
<div>
<p className="mb-2 text-sm font-medium text-white/90">Your response</p>
<div className="flex gap-2">
{rsvpButtons.map(({ status, label, icon: Icon }) => {
const active = myResponse === status
return (
<button
key={status}
onClick={() => respond(status)}
disabled={busy !== null}
className={`flex flex-1 items-center justify-center gap-1.5 rounded-xl border px-3 py-2.5 text-sm font-medium transition-colors disabled:opacity-70 ${
active
? "border-white/40 bg-white/30 text-white"
: "border-white/20 bg-white/10 text-white/80 hover:bg-white/20"
}`}
>
{busy === `respond-${status}` ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Icon className="h-4 w-4" />
)}
{label}
</button>
)
})}
</div>
<p className="mt-2 text-xs text-white/60">
{goingCount} going · {maybeCount} maybe · {declinedCount} declined
</p>
</div>
)}
{error && (
<div className="rounded-xl border border-red-400/40 bg-red-500/20 px-4 py-2.5 text-sm">
{error}
</div>
)}
{/* Management actions */}
{canManage && (
<div className="flex flex-wrap gap-2 border-t border-white/15 pt-4">
{event.status !== "confirmed" && event.status !== "cancelled" && (
<button
onClick={() => run("confirm", () => api.confirmEvent(event.id))}
disabled={busy !== null}
className="flex items-center gap-1.5 rounded-xl bg-emerald-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-emerald-500 disabled:opacity-70"
>
<CheckCircle2 className="h-4 w-4" /> Confirm
</button>
)}
{event.status !== "cancelled" && (
<button
onClick={() => run("cancel", () => api.cancelEvent(event.id))}
disabled={busy !== null}
className="flex items-center gap-1.5 rounded-xl bg-white/10 px-3 py-2 text-sm font-medium transition-colors hover:bg-white/20 disabled:opacity-70"
>
<Ban className="h-4 w-4" /> Cancel
</button>
)}
<button
onClick={() => run("delete", () => api.deleteEvent(event.id))}
disabled={busy !== null}
className="ml-auto flex items-center gap-1.5 rounded-xl bg-red-500/80 px-3 py-2 text-sm font-medium transition-colors hover:bg-red-500 disabled:opacity-70"
>
<Trash2 className="h-4 w-4" /> Delete
</button>
</div>
)}
</div>
</GlassModal>
)
}