This commit is contained in:
ben
2026-08-20 12:23:30 +02:00
parent e61750f314
commit a980812137
9 changed files with 351 additions and 106 deletions
@@ -14,10 +14,13 @@ import {
CheckCircle2,
Ban,
Loader2,
Pencil,
UserRound,
} from "lucide-react"
import { GlassModal } from "./glass-modal"
import * as api from "@/lib/api"
import { ApiError } from "@/lib/api"
import { useRoomMembers } from "@/lib/hooks"
import type { CalendarEvent, Room, SignupStatus } from "@/lib/types"
import { getRoomColor } from "@/lib/room-theme"
import { parseDateTime, formatDayLabel, formatTime } from "@/lib/date-utils"
@@ -28,6 +31,7 @@ interface EventDetailDialogProps {
currentUserId: number
onClose: () => void
onChanged: () => void
onEdit: () => void
}
const STATUS_STYLES: Record<string, string> = {
@@ -43,6 +47,7 @@ export function EventDetailDialog({
currentUserId,
onClose,
onChanged,
onEdit,
}: EventDetailDialogProps) {
const [busy, setBusy] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
@@ -52,11 +57,16 @@ export function EventDetailDialog({
["responses", event.id],
() => api.getEventResponses(event.id),
)
const { members } = useRoomMembers(event.room_id)
const start = parseDateTime(event.start_time)
const end = parseDateTime(event.end_time)
const color = getRoomColor(event.room_id)
const creator = members.find((m) => m.id === event.creator_id)
const creatorLabel =
event.creator_id === currentUserId ? "you" : (creator?.username ?? `member #${event.creator_id}`)
const canManage =
event.creator_id === currentUserId || room?.role === "admin" || room?.role === "owner"
@@ -79,6 +89,20 @@ export function EventDetailDialog({
}
}
const handleDelete = async () => {
setBusy("delete")
setError(null)
try {
await api.deleteEvent(event.id)
onChanged()
onClose()
} 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)
@@ -94,13 +118,15 @@ export function EventDetailDialog({
<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>
{event.type === "proposal" && (
<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">
@@ -116,6 +142,10 @@ export function EventDetailDialog({
<DoorClosed className="h-4 w-4 text-white/70" />
{event.type} · {event.visibility.replace("_", " ")}
</p>
<p className="flex items-center gap-2">
<UserRound className="h-4 w-4 text-white/70" />
Created by {creatorLabel}
</p>
{event.type === "proposal" && (
<p className="flex items-center gap-2">
<Users className="h-4 w-4 text-white/70" />
@@ -130,8 +160,8 @@ export function EventDetailDialog({
</p>
)}
{/* RSVP */}
{event.status !== "cancelled" && (
{/* RSVP — only relevant for proposals, busy/private have no attendees to respond */}
{event.type === "proposal" && event.status !== "cancelled" && (
<div>
<p className="mb-2 text-sm font-medium text-white/90">Your response</p>
<div className="flex gap-2">
@@ -174,22 +204,31 @@ export function EventDetailDialog({
{canManage && (
<div className="flex flex-col gap-3 border-t border-white/15 pt-4">
<div className="flex flex-wrap gap-2">
{event.status !== "confirmed" && event.status !== "cancelled" && (
<button
onClick={onEdit}
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"
>
<Pencil className="h-4 w-4" /> Edit event
</button>
{/* Confirm/cancel only make sense for proposals — busy/private
blocks are already "confirmed" the moment they're created. */}
{event.type === "proposal" && 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
<CheckCircle2 className="h-4 w-4" /> Confirm event
</button>
)}
{event.status !== "cancelled" && (
{event.type === "proposal" && 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
<Ban className="h-4 w-4" /> Cancel event
</button>
)}
</div>
@@ -214,7 +253,7 @@ export function EventDetailDialog({
<div className="flex flex-col gap-2">
{/* Destructive action sits on top, off the "usual" spot. */}
<button
onClick={() => run("delete", () => api.deleteEvent(event.id))}
onClick={handleDelete}
disabled={busy !== null}
className="flex items-center justify-center gap-1.5 rounded-xl bg-red-500 px-3 py-2.5 text-sm font-medium transition-colors hover:bg-red-600 disabled:opacity-70"
>
@@ -239,6 +278,16 @@ export function EventDetailDialog({
</div>
</div>
)}
{/* Always-available close action, bottom right. */}
<div className="flex justify-end pt-1">
<button
onClick={onClose}
className="rounded-xl border-2 border-emerald-400/80 bg-white/5 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-500/20"
>
OK
</button>
</div>
</div>
</GlassModal>
)