msg
This commit is contained in:
@@ -6,21 +6,23 @@ 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"
|
||||
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 (blocked time)" },
|
||||
{ value: "private", label: "Private" },
|
||||
{ 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 }[] = [
|
||||
@@ -35,27 +37,49 @@ export function CreateEventDialog({
|
||||
rooms,
|
||||
defaultRoomId,
|
||||
defaultDate,
|
||||
editingEvent,
|
||||
onClose,
|
||||
onCreated,
|
||||
}: CreateEventDialogProps) {
|
||||
const initialRoom = defaultRoomId ?? rooms[0]?.id ?? 0
|
||||
const isEditing = !!editingEvent
|
||||
const initialRoom = editingEvent?.room_id ?? 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)
|
||||
// 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("")
|
||||
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 [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)
|
||||
@@ -64,38 +88,51 @@ export function CreateEventDialog({
|
||||
setError("Please select a room.")
|
||||
return
|
||||
}
|
||||
const startDate = new Date(start)
|
||||
const endDate = new Date(end)
|
||||
if (endDate <= startDate) {
|
||||
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 {
|
||||
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,
|
||||
})
|
||||
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 : "Could not create the event.")
|
||||
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="Create event" onClose={onClose}>
|
||||
<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>
|
||||
@@ -104,7 +141,7 @@ export function CreateEventDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<GlassModal title="Create event" onClose={onClose}>
|
||||
<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">
|
||||
@@ -152,33 +189,83 @@ export function CreateEventDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-xl border border-white/15 bg-white/5 p-3">
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="event-start">
|
||||
Start
|
||||
<label className={labelClass} htmlFor="event-date">
|
||||
Date
|
||||
</label>
|
||||
<input
|
||||
id="event-start"
|
||||
type="datetime-local"
|
||||
id="event-date"
|
||||
type="date"
|
||||
required
|
||||
value={start}
|
||||
onChange={(e) => setStart(e.target.value)}
|
||||
value={date}
|
||||
onChange={(e) => {
|
||||
setDate(e.target.value)
|
||||
if (!multiDay) setEndDate(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 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>
|
||||
@@ -188,7 +275,7 @@ export function CreateEventDialog({
|
||||
<select
|
||||
id="event-type"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as EventType)}
|
||||
onChange={(e) => handleTypeChange(e.target.value as EventType)}
|
||||
className={fieldClass}
|
||||
>
|
||||
{TYPE_OPTIONS.map((o) => (
|
||||
@@ -218,23 +305,25 @@ export function CreateEventDialog({
|
||||
</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>
|
||||
{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">
|
||||
@@ -256,7 +345,7 @@ export function CreateEventDialog({
|
||||
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
|
||||
{isEditing ? "Save changes" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user