From de03f9129b93cd7da3253e1de01a06433389c108 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Sun, 24 May 2026 22:37:11 -0400 Subject: [PATCH] feat: add mobile Drawer branch with guided steps to EventDialog --- src/components/event-dialog.tsx | 297 ++++++++++++++++++-------------- 1 file changed, 163 insertions(+), 134 deletions(-) diff --git a/src/components/event-dialog.tsx b/src/components/event-dialog.tsx index 9e1988d..d8634d6 100644 --- a/src/components/event-dialog.tsx +++ b/src/components/event-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { addHours, addMinutes, isValid, parseISO } from "date-fns"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { DateTimePicker } from "@/components/date-time-picker"; import { LocationAutocomplete } from "@/components/location-autocomplete"; @@ -16,6 +16,15 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, +} from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -62,27 +71,83 @@ export const EventDialog = ({ register, reset, setError, + setValue, + trigger, watch, formState: { errors }, } = useForm({ defaultValues: getDefaultEventFormValues(), }); + const [step, setStep] = useState<1 | 2 | 3>(1); + const advanceStep = () => setStep((s) => Math.min(s + 1, 3) as 1 | 2 | 3); + const retreatStep = () => setStep((s) => Math.max(s - 1, 1) as 1 | 2 | 3); + useEffect(() => { reset(initialValues); }, [initialValues, reset]); - const allDay = watch("allDay"); - const start = watch("start"); - const handleOpenChange = (nextOpen: boolean) => { if (!nextOpen) { reset(getDefaultEventFormValues()); + setStep(1); onReset(); } onOpenChange(nextOpen); }; + const STEP_FIELDS: Record<1 | 2 | 3, (keyof EventFormValues)[]> = { + 1: ["title", "url"], + 2: ["start", "end"], + 3: ["recurrenceRule"], + }; + + const handleNext = async () => { + const valid = await trigger(STEP_FIELDS[step]); + if (valid) advanceStep(); + }; + + const handleSave = handleSubmit((values) => { + const result = validateEventFormValues(values); + if (!result.success) { + const fieldErrors = result.error.flatten().fieldErrors; + let firstErrorStep: 1 | 2 | 3 | null = null; + for (const [fieldName, messages] of Object.entries(fieldErrors)) { + const firstMessage = messages?.[0]; + if (firstMessage) { + setError(fieldName as keyof EventFormValues, { message: firstMessage }); + const ownerStep = ( + Object.entries(STEP_FIELDS) as [string, (keyof EventFormValues)[]][] + ).find(([, fields]) => fields.includes(fieldName as keyof EventFormValues))?.[0]; + const ownerStepNum = ownerStep ? (Number(ownerStep) as 1 | 2 | 3) : null; + if (ownerStepNum !== null && (firstErrorStep === null || ownerStepNum < firstErrorStep)) { + firstErrorStep = ownerStepNum; + } + } + } + if (firstErrorStep !== null) setStep(firstErrorStep); + 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.", + }); + setStep(3); + return; + } + } + + onSave(result.data); + reset(getDefaultEventFormValues()); + }); + const onSubmit = handleSubmit((values) => { const result = validateEventFormValues(values); if (!result.success) { @@ -116,142 +181,106 @@ export const EventDialog = ({ reset(getDefaultEventFormValues()); }); - return ( - - - - {titleText} - {descriptionText} - + const stepProps = { control, register, errors, watch, setValue, isAiDraft }; -
- {/* TODO(Task 4): replace this inline banner with */} - {isAiDraft && ( -
- This draft was generated from natural language. Double-check dates, times, location, - recurrence, and links before saving. -
+ const progressBars = ( +
+ {([1, 2, 3] as const).map((n) => ( +
+ ))} +
+ ); -
-

Event details

-
- - - {errors.title &&

{errors.title.message}

} + const stepTitles: Record<1 | 2 | 3, string> = { + 1: "Event Details", + 2: "Schedule", + 3: "Recurrence", + }; + + if (!isMobile) { + return ( + + + + {titleText} + {descriptionText} + + + {isAiDraft && } +
+

Event details

+ +
+
+

Schedule

+ +
+
+

Recurrence

+ +
+ + + + + +
+
+ ); + } + + return ( + + + +
+
+ {stepTitles[step]} + + {step === 1 && "Title and start date are required."} + {step === 2 && "Set your event's date and time."} + {step === 3 && "Optionally repeat this event."} +
- -
- -