Files
calendar/frontend(experimental)/newchat/components/create-event-dialog.tsx
T
2026-08-20 12:23:30 +02:00

355 lines
12 KiB
TypeScript

"use client"
import type React from "react"
import { useState } from "react"
import { Loader2 } from "lucide-react"
import { GlassModal, fieldClass, labelClass } from "./glass-modal"
import * as api from "@/lib/api"
import { ApiError } from "@/lib/api"
import type { CalendarEvent, EventType, EventVisibility, Room } from "@/lib/types"
import { parseDateTime, toBackendDateTime, toDateParam, toInputTime } from "@/lib/date-utils"
interface CreateEventDialogProps {
rooms: Room[]
defaultRoomId: number | null
defaultDate: Date
/** When set, the dialog edits this event instead of creating a new one. */
editingEvent?: CalendarEvent
onClose: () => void
onCreated: () => void
}
const TYPE_OPTIONS: { value: EventType; label: string }[] = [
{ value: "proposal", label: "Proposal (members vote to confirm)" },
{ value: "busy", label: "Busy — room sees it as busy, no details" },
{ value: "private", label: "Private — hidden from the room" },
]
const VISIBILITY_OPTIONS: { value: EventVisibility; label: string }[] = [
{ value: "room", label: "Room — visible to all members" },
{ value: "busy_only", label: "Busy only — hide details" },
{ value: "private", label: "Private — only me" },
]
const optionClass = "bg-slate-800 text-white"
export function CreateEventDialog({
rooms,
defaultRoomId,
defaultDate,
editingEvent,
onClose,
onCreated,
}: CreateEventDialogProps) {
const isEditing = !!editingEvent
const initialRoom = editingEvent?.room_id ?? defaultRoomId ?? rooms[0]?.id ?? 0
// Start time defaults to "now", rounded up to the next quarter hour.
const now = new Date()
const defaultStartTime = new Date(now)
defaultStartTime.setMinutes(Math.ceil(now.getMinutes() / 15) * 15, 0, 0)
const defaultEndTime = new Date(defaultStartTime)
defaultEndTime.setHours(defaultEndTime.getHours() + 1)
const editStart = editingEvent ? parseDateTime(editingEvent.start_time) : null
const editEnd = editingEvent ? parseDateTime(editingEvent.end_time) : null
const [roomId, setRoomId] = useState<number>(initialRoom)
const [title, setTitle] = useState(editingEvent?.title ?? "")
const [description, setDescription] = useState(editingEvent?.description ?? "")
const [type, setType] = useState<EventType>(editingEvent?.type ?? "proposal")
const [visibility, setVisibility] = useState<EventVisibility>(editingEvent?.visibility ?? "room")
const [date, setDate] = useState(toDateParam(editStart ?? defaultDate))
const [multiDay, setMultiDay] = useState(
editStart && editEnd ? toDateParam(editStart) !== toDateParam(editEnd) : false,
)
const [endDate, setEndDate] = useState(toDateParam(editEnd ?? editStart ?? defaultDate))
const [startTime, setStartTime] = useState(toInputTime(editStart ?? defaultStartTime))
const [endTime, setEndTime] = useState(toInputTime(editEnd ?? defaultEndTime))
const [minPeople, setMinPeople] = useState(editingEvent?.min_people ?? 1)
const [error, setError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
// Busy/private events are always visible to the room as just "busy" (no
// details) or not visible at all — that's implied by the type, so the
// visibility choice only makes sense for proposals.
const handleTypeChange = (value: EventType) => {
setType(value)
if (value === "busy") setVisibility("busy_only")
else if (value === "private") setVisibility("private")
else setVisibility("room")
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
if (!roomId) {
setError("Please select a room.")
return
}
const startDate = new Date(`${date}T${startTime}`)
const endDate2 = new Date(`${multiDay ? endDate : date}T${endTime}`)
if (endDate2 <= startDate) {
setError("End time must be after the start time.")
return
}
const payload = {
room_id: roomId,
type,
title: title.trim(),
description: description.trim(),
start_time: toBackendDateTime(startDate),
end_time: toBackendDateTime(endDate2),
visibility: type === "proposal" ? visibility : type === "busy" ? "busy_only" : "private",
min_people: type === "proposal" ? minPeople : 0,
} as const
setSubmitting(true)
try {
if (isEditing && editingEvent) {
await api.updateEvent(editingEvent.id, payload)
} else {
await api.createEvent({ ...payload, status: type === "proposal" ? "proposed" : "confirmed" })
}
onCreated()
onClose()
} catch (err) {
setError(
err instanceof ApiError
? err.message
: isEditing
? "Could not save the changes."
: "Could not create the event.",
)
} finally {
setSubmitting(false)
}
}
const dialogTitle = isEditing ? "Edit event" : "Create event"
if (rooms.length === 0) {
return (
<GlassModal title={dialogTitle} onClose={onClose}>
<p className="text-sm text-white/80">
You need to be in a room before creating an event. Create or join a room first.
</p>
</GlassModal>
)
}
return (
<GlassModal title={dialogTitle} onClose={onClose} maxWidth="max-w-lg">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<label className={labelClass} htmlFor="event-room">
Room
</label>
<select
id="event-room"
value={roomId}
onChange={(e) => setRoomId(Number(e.target.value))}
className={fieldClass}
>
{rooms.map((r) => (
<option key={r.id} value={r.id} className={optionClass}>
{r.name}
</option>
))}
</select>
</div>
<div>
<label className={labelClass} htmlFor="event-title">
Title
</label>
<input
id="event-title"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
className={fieldClass}
placeholder="e.g. Team dinner"
/>
</div>
<div>
<label className={labelClass} htmlFor="event-desc">
Description
</label>
<textarea
id="event-desc"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
className={fieldClass}
placeholder="Optional details"
/>
</div>
<div className="rounded-xl border border-white/15 bg-white/5 p-3">
<div>
<label className={labelClass} htmlFor="event-date">
Date
</label>
<input
id="event-date"
type="date"
required
value={date}
onChange={(e) => {
setDate(e.target.value)
if (!multiDay) setEndDate(e.target.value)
}}
className={`${fieldClass} [color-scheme:dark]`}
/>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<div>
<label className={labelClass} htmlFor="event-start-time">
Start time
</label>
<input
id="event-start-time"
type="time"
required
lang="nl-NL"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className={`${fieldClass} [color-scheme:dark]`}
/>
</div>
<div>
<label className={labelClass} htmlFor="event-end-time">
End time
</label>
<input
id="event-end-time"
type="time"
required
lang="nl-NL"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className={`${fieldClass} [color-scheme:dark]`}
/>
</div>
</div>
<label className="mt-3 flex items-center gap-2 text-xs text-white/70">
<input
type="checkbox"
checked={multiDay}
onChange={(e) => {
setMultiDay(e.target.checked)
if (!e.target.checked) setEndDate(date)
}}
className="h-3.5 w-3.5 rounded border-white/30 bg-white/10"
/>
Ends on a different day
</label>
{multiDay && (
<div className="mt-2">
<label className={labelClass} htmlFor="event-end-date">
End date
</label>
<input
id="event-end-date"
type="date"
required
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className={`${fieldClass} [color-scheme:dark]`}
/>
</div>
)}
</div>
<div>
<label className={labelClass} htmlFor="event-type">
Type
</label>
<select
id="event-type"
value={type}
onChange={(e) => handleTypeChange(e.target.value as EventType)}
className={fieldClass}
>
{TYPE_OPTIONS.map((o) => (
<option key={o.value} value={o.value} className={optionClass}>
{o.label}
</option>
))}
</select>
</div>
{type === "proposal" && (
<div>
<label className={labelClass} htmlFor="event-min">
Minimum people to confirm
</label>
<input
id="event-min"
type="number"
min={0}
value={minPeople}
onChange={(e) => setMinPeople(Number(e.target.value))}
className={fieldClass}
/>
<p className="mt-1 text-xs text-white/60">
Auto-confirms once this many members respond &quot;going&quot;.
</p>
</div>
)}
{type === "proposal" && (
<div>
<label className={labelClass} htmlFor="event-visibility">
Visibility
</label>
<select
id="event-visibility"
value={visibility}
onChange={(e) => setVisibility(e.target.value as EventVisibility)}
className={fieldClass}
>
{VISIBILITY_OPTIONS.map((o) => (
<option key={o.value} value={o.value} className={optionClass}>
{o.label}
</option>
))}
</select>
</div>
)}
{error && (
<div className="rounded-xl border border-red-400/40 bg-red-500/20 px-4 py-2.5 text-sm">
{error}
</div>
)}
<div className="mt-1 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 text-sm font-medium transition-colors hover:bg-white/20"
>
Cancel
</button>
<button
type="submit"
disabled={submitting}
className="flex items-center gap-2 rounded-xl bg-blue-500 px-5 py-2.5 text-sm font-medium transition-colors hover:bg-blue-600 disabled:opacity-70"
>
{submitting && <Loader2 className="h-4 w-4 animate-spin" />}
{isEditing ? "Save changes" : "Create"}
</button>
</div>
</form>
</GlassModal>
)
}