266 lines
7.9 KiB
TypeScript
266 lines
7.9 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 { EventType, EventVisibility, Room } from "@/lib/types"
|
|
import { toBackendDateTime, toInputDateTime } from "@/lib/date-utils"
|
|
|
|
interface CreateEventDialogProps {
|
|
rooms: Room[]
|
|
defaultRoomId: number | null
|
|
defaultDate: Date
|
|
onClose: () => void
|
|
onCreated: () => void
|
|
}
|
|
|
|
const TYPE_OPTIONS: { value: EventType; label: string }[] = [
|
|
{ value: "proposal", label: "Proposal (members vote to confirm)" },
|
|
{ value: "busy", label: "Busy (blocked time)" },
|
|
{ value: "private", label: "Private" },
|
|
]
|
|
|
|
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,
|
|
onClose,
|
|
onCreated,
|
|
}: CreateEventDialogProps) {
|
|
const initialRoom = defaultRoomId ?? rooms[0]?.id ?? 0
|
|
|
|
const defaultStart = new Date(defaultDate)
|
|
defaultStart.setHours(9, 0, 0, 0)
|
|
const defaultEnd = new Date(defaultDate)
|
|
defaultEnd.setHours(10, 0, 0, 0)
|
|
|
|
const [roomId, setRoomId] = useState<number>(initialRoom)
|
|
const [title, setTitle] = useState("")
|
|
const [description, setDescription] = useState("")
|
|
const [type, setType] = useState<EventType>("proposal")
|
|
const [visibility, setVisibility] = useState<EventVisibility>("room")
|
|
const [start, setStart] = useState(toInputDateTime(defaultStart))
|
|
const [end, setEnd] = useState(toInputDateTime(defaultEnd))
|
|
const [minPeople, setMinPeople] = useState(1)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError(null)
|
|
|
|
if (!roomId) {
|
|
setError("Please select a room.")
|
|
return
|
|
}
|
|
const startDate = new Date(start)
|
|
const endDate = new Date(end)
|
|
if (endDate <= startDate) {
|
|
setError("End time must be after the start time.")
|
|
return
|
|
}
|
|
|
|
setSubmitting(true)
|
|
try {
|
|
await api.createEvent({
|
|
room_id: roomId,
|
|
type,
|
|
title: title.trim(),
|
|
description: description.trim(),
|
|
start_time: toBackendDateTime(startDate),
|
|
end_time: toBackendDateTime(endDate),
|
|
visibility,
|
|
status: type === "proposal" ? "proposed" : "confirmed",
|
|
min_people: type === "proposal" ? minPeople : 0,
|
|
})
|
|
onCreated()
|
|
onClose()
|
|
} catch (err) {
|
|
setError(err instanceof ApiError ? err.message : "Could not create the event.")
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
if (rooms.length === 0) {
|
|
return (
|
|
<GlassModal title="Create event" 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="Create event" onClose={onClose}>
|
|
<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="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass} htmlFor="event-start">
|
|
Start
|
|
</label>
|
|
<input
|
|
id="event-start"
|
|
type="datetime-local"
|
|
required
|
|
value={start}
|
|
onChange={(e) => setStart(e.target.value)}
|
|
className={`${fieldClass} [color-scheme:dark]`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass} htmlFor="event-end">
|
|
End
|
|
</label>
|
|
<input
|
|
id="event-end"
|
|
type="datetime-local"
|
|
required
|
|
value={end}
|
|
onChange={(e) => setEnd(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) => setType(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 "going".
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<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" />}
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</GlassModal>
|
|
)
|
|
}
|