new frontend
ik ben zo cool
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user