Files
calendar/frontend(experimental)/newchat/components/event-detail-dialog.tsx
T
ben bf601846db new frontend
ik ben zo cool
2026-08-19 09:33:48 +02:00

206 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
)
}