From 260b77ee10759303032bc81eeddb6afc4249bb7c Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Sun, 24 May 2026 22:19:27 -0400 Subject: [PATCH] refactor: extract DetailsStep, ScheduleStep, RecurrenceStep into EventDialog --- src/components/event-dialog.tsx | 754 +++++++++++++++++++------------- 1 file changed, 447 insertions(+), 307 deletions(-) diff --git a/src/components/event-dialog.tsx b/src/components/event-dialog.tsx index 868f6f8..25f275a 100644 --- a/src/components/event-dialog.tsx +++ b/src/components/event-dialog.tsx @@ -9,344 +9,484 @@ import { RecurrencePicker } from "@/components/recurrence-picker"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useIsMobile } from "@/hooks/use-mobile"; import { - type EventFormValues, - getDefaultEventFormValues, - validateEventFormValues, + type EventFormValues, + getDefaultEventFormValues, + validateEventFormValues, } from "@/lib/event-form"; import { parseRecurrenceRule, validateRecurrence } from "@/lib/recurrence"; +import { cn } from "@/lib/utils"; interface EventDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - editingId: string | null; - dialogSource: "manual" | "ai"; - initialValues: EventFormValues; - onSave: (values: EventFormValues) => void; - onReset: () => void; + open: boolean; + onOpenChange: (open: boolean) => void; + editingId: string | null; + dialogSource: "manual" | "ai"; + initialValues: EventFormValues; + onSave: (values: EventFormValues) => void; + onReset: () => void; } export const EventDialog = ({ - open, - onOpenChange, - editingId, - dialogSource, - initialValues, - onSave, - onReset, + open, + onOpenChange, + editingId, + dialogSource, + initialValues, + onSave, + onReset, }: EventDialogProps) => { - const isMobile = useIsMobile(); - const isAiDraft = dialogSource === "ai" && !editingId; - const titleText = editingId - ? "Edit Event" - : isAiDraft - ? "Review AI Draft" - : "New Event"; - const descriptionText = editingId - ? "Update the event details below. Title and start date are required." - : isAiDraft - ? "AI filled in this event from your prompt. Review each field, then save when it looks right." - : "Create an event manually. Title and start date are required."; - const saveLabel = editingId ? "Update Event" : "Save Event"; + const isMobile = useIsMobile(); + const isAiDraft = dialogSource === "ai" && !editingId; + const titleText = editingId ? "Edit Event" : isAiDraft ? "Review AI Draft" : "New Event"; + const descriptionText = editingId + ? "Update the event details below. Title and start date are required." + : isAiDraft + ? "AI filled in this event from your prompt. Review each field, then save when it looks right." + : "Create an event manually. Title and start date are required."; + const saveLabel = editingId ? "Update Event" : "Save Event"; - const { - control, - handleSubmit, - register, - reset, - setError, - setValue, - watch, - formState: { errors }, - } = useForm({ - defaultValues: getDefaultEventFormValues(), - }); + const { + control, + handleSubmit, + register, + reset, + setError, + setValue, + watch, + formState: { errors }, + } = useForm({ + defaultValues: getDefaultEventFormValues(), + }); - useEffect(() => { - reset(initialValues); - }, [initialValues, reset]); + useEffect(() => { + reset(initialValues); + }, [initialValues, reset]); - const allDay = watch("allDay"); - const start = watch("start"); + const allDay = watch("allDay"); + const start = watch("start"); - const handleOpenChange = (nextOpen: boolean) => { - if (!nextOpen) { - reset(getDefaultEventFormValues()); - onReset(); - } - onOpenChange(nextOpen); - }; + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) { + reset(getDefaultEventFormValues()); + onReset(); + } + onOpenChange(nextOpen); + }; - const DURATIONS = [ - { label: "+15 min", minutes: 15 }, - { label: "+30 min", minutes: 30 }, - { label: "+1 hour", minutes: 60 }, - { label: "+3 hours", minutes: 180 }, - ]; + const DURATIONS = [ + { label: "+15 min", minutes: 15 }, + { label: "+30 min", minutes: 30 }, + { label: "+1 hour", minutes: 60 }, + { label: "+3 hours", minutes: 180 }, + ]; - const handleApplyDuration = ( - minutes: number, - currentAllDay: boolean, - currentStart: string, - ) => { - if (!currentStart) return; - const base = parseISO(currentStart); - if (!isValid(base)) return; - const next = - minutes < 60 ? addMinutes(base, minutes) : addHours(base, minutes / 60); - const pad = (value: number) => String(value).padStart(2, "0"); - const result = currentAllDay - ? `${next.getFullYear()}-${pad(next.getMonth() + 1)}-${pad(next.getDate())}` - : `${next.getFullYear()}-${pad(next.getMonth() + 1)}-${pad(next.getDate())}T${pad(next.getHours())}:${pad(next.getMinutes())}:00`; - setValue("end", result, { shouldDirty: true }); - }; + const handleApplyDuration = (minutes: number, currentAllDay: boolean, currentStart: string) => { + if (!currentStart) return; + const base = parseISO(currentStart); + if (!isValid(base)) return; + const next = minutes < 60 ? addMinutes(base, minutes) : addHours(base, minutes / 60); + const pad = (value: number) => String(value).padStart(2, "0"); + const result = currentAllDay + ? `${next.getFullYear()}-${pad(next.getMonth() + 1)}-${pad(next.getDate())}` + : `${next.getFullYear()}-${pad(next.getMonth() + 1)}-${pad(next.getDate())}T${pad(next.getHours())}:${pad(next.getMinutes())}:00`; + setValue("end", result, { shouldDirty: true }); + }; - const onSubmit = handleSubmit((values) => { - const result = validateEventFormValues(values); - if (!result.success) { - const fieldErrors = result.error.flatten().fieldErrors; - for (const [fieldName, messages] of Object.entries(fieldErrors)) { - const firstMessage = messages?.[0]; - if (firstMessage) { - setError(fieldName as keyof EventFormValues, { - message: firstMessage, - }); - } - } - return; - } + const onSubmit = handleSubmit((values) => { + const result = validateEventFormValues(values); + if (!result.success) { + const fieldErrors = result.error.flatten().fieldErrors; + for (const [fieldName, messages] of Object.entries(fieldErrors)) { + const firstMessage = messages?.[0]; + if (firstMessage) { + setError(fieldName as keyof EventFormValues, { + message: firstMessage, + }); + } + } + return; + } - if (values.recurrenceRule) { - const recurrenceValidation = validateRecurrence( - parseRecurrenceRule(values.recurrenceRule), - ); - if (!recurrenceValidation.isValid) { - setError("recurrenceRule", { - message: - recurrenceValidation.errors.rule || - recurrenceValidation.errors.count || - recurrenceValidation.errors.until || - "Invalid recurrence.", - }); - return; - } - } + if (values.recurrenceRule) { + const recurrenceValidation = validateRecurrence(parseRecurrenceRule(values.recurrenceRule)); + if (!recurrenceValidation.isValid) { + setError("recurrenceRule", { + message: + recurrenceValidation.errors.rule || + recurrenceValidation.errors.count || + recurrenceValidation.errors.until || + "Invalid recurrence.", + }); + return; + } + } - onSave(result.data); - reset(getDefaultEventFormValues()); - }); + onSave(result.data); + reset(getDefaultEventFormValues()); + }); - return ( - - - - - {titleText} - - {descriptionText} - + return ( + + + + {titleText} + {descriptionText} + -
- {isAiDraft && ( -
- This draft was generated from natural language. Double-check - dates, times, location, recurrence, and links before saving. -
- )} + + {isAiDraft && ( +
+ This draft was generated from natural language. Double-check dates, times, location, + recurrence, and links before saving. +
+ )} -
-

- Event details -

-
- - - {errors.title && ( -

- {errors.title.message} -

- )} -
+
+

Event details

+
+ + + {errors.title &&

{errors.title.message}

} +
-
- -