diff --git a/apps/ui/package.json b/apps/ui/package.json index fc45274ff..a4e362af9 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -15,6 +15,7 @@ "jotai": "^2.15.1", "lucide-react": "^0.555.0", "marked": "^18.0.5", + "motion": "12.42.0", "next": "16.2.5", "react": "^19.2.6", "react-day-picker": "^9.13.0", diff --git a/apps/ui/public/r/booker-1.json b/apps/ui/public/r/booker-1.json index 4e004f553..d6e086d4d 100644 --- a/apps/ui/public/r/booker-1.json +++ b/apps/ui/public/r/booker-1.json @@ -5,7 +5,8 @@ "dependencies": [ "isomorphic-dompurify", "marked", - "lucide-react" + "lucide-react", + "motion" ], "registryDependencies": [ "@coss/avatar", @@ -14,26 +15,41 @@ "@coss/card", "@coss/combobox", "@coss/empty", + "@coss/field", + "@coss/form", + "@coss/group", "@coss/input", "@coss/label", "@coss/popover", "@coss/scroll-area", "@coss/select", - "@coss/size-transition", "@coss/skeleton", "@coss/switch", + "@coss/textarea", "@coss/tooltip" ], "files": [ { "path": "registry/default/atoms/booker-1.tsx", - "content": "\"use client\";\n\nimport { ArrowLeftIcon, Clock3Icon } from \"lucide-react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Card } from \"@/registry/default/ui/card\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport {\n SizeTransition,\n SizeTransitionPanel,\n} from \"@/registry/default/ui/size-transition\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport { BookerAvatars } from \"./booker/booker-avatars\";\nimport { BookerCalendar } from \"./booker/booker-calendar\";\nimport { BookerErrorState } from \"./booker/booker-error-state\";\nimport { type BookerLabels, getBookerLabels } from \"./booker/booker-labels\";\nimport { DurationPicker } from \"./booker/duration-picker\";\nimport { EventDescription } from \"./booker/event-description\";\nimport { HeaderBanner } from \"./booker/header-banner\";\nimport { Location } from \"./booker/location\";\nimport { TimePicker } from \"./booker/time-picker\";\nimport { TimezonePicker } from \"./booker/timezone-picker\";\nimport type { BookerTarget } from \"@/lib/booker/target\";\nimport { type BookerInitialData, useBooker } from \"@/lib/booker/use-booker\";\n\ntype BookerProps = {\n target: BookerTarget;\n timezone?: string;\n initialData?: BookerInitialData;\n defaultFormValues?: Record;\n onCreateBookingSuccess?: (data: unknown) => void;\n labels?: Partial;\n};\n\nexport function Booker({ initialData, target, timezone, labels }: BookerProps) {\n const t = getBookerLabels(labels);\n const booker = useBooker({ initialData, target, timezone });\n\n if (booker.error) {\n return (\n \n );\n }\n\n const displayMeta = booker.meta;\n const headerImageAlt = displayMeta\n ? t.headerImageAlt(displayMeta.hostName, displayMeta.eventTypeTitle)\n : \"Booker header\";\n const metaContentClassName = [\n displayMeta?.eventTypeImageUrl ? \"relative -mt-11 @5xl:-mt-13\" : \"\",\n \"flex flex-col gap-6 @5xl:p-6 p-4\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n \n \n {/* Meta */}\n
\n \n
\n
\n
\n \n {displayMeta ? (\n

\n {displayMeta.hostName}\n

\n ) : (\n \n )}\n
\n
\n {displayMeta ? (\n <>\n

\n {displayMeta.eventTypeTitle}\n

\n \n \n ) : (\n <>\n \n \n \n \n )}\n
\n
\n\n {displayMeta ? (\n
\n {displayMeta.eventTypeDurationOptions ? (\n \n ) : (\n
\n \n \n {displayMeta.eventTypeDurationMinutes\n ? t.durationMinutes(\n displayMeta.eventTypeDurationMinutes,\n )\n : t.durationUnknown}\n \n
\n )}\n \n \n
\n ) : (\n
\n \n \n \n
\n )}\n
\n
\n \n }\n >\n {booker.step === \"select\" ? (\n }\n >\n {/* Calendar */}\n
\n \n
\n {/* Time picker */}\n \n \n ) : (\n \n }\n >\n \n \n \n )}\n \n
\n \n Cal.com\n \n \n );\n}\n\nexport default Booker;\n", + "content": "\"use client\";\n\nimport { CalendarIcon, Clock3Icon } from \"lucide-react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Card } from \"@/registry/default/ui/card\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport { BookerAvatars } from \"./booker/booker-avatars\";\nimport { BookerCalendar } from \"./booker/booker-calendar\";\nimport { BookerConfirmForm } from \"./booker/booker-confirm-form\";\nimport { BookerErrorState } from \"./booker/booker-error-state\";\nimport { type BookerLabels, getBookerLabels } from \"./booker/booker-labels\";\nimport { BookerSteps } from \"./booker/booker-steps\";\nimport { DurationPicker } from \"./booker/duration-picker\";\nimport { EventDescription } from \"./booker/event-description\";\nimport { HeaderBanner } from \"./booker/header-banner\";\nimport { Location } from \"./booker/location\";\nimport { TimePicker } from \"./booker/time-picker\";\nimport { TimezoneDisplay, TimezonePicker } from \"./booker/timezone-picker\";\nimport type { BookerTarget } from \"@/lib/booker/target\";\nimport { type BookerInitialData, useBooker } from \"@/lib/booker/use-booker\";\nimport {\n formatConfirmDateDetail,\n formatConfirmTimeRange,\n formatSelectedWeekday,\n} from \"@/lib/booker/utils\";\n\ntype BookerProps = {\n target: BookerTarget;\n timezone?: string;\n initialData?: BookerInitialData;\n defaultFormValues?: Record;\n onCreateBookingSuccess?: (data: unknown) => void;\n labels?: Partial;\n};\n\nexport function Booker({\n initialData,\n target,\n timezone,\n labels,\n defaultFormValues,\n onCreateBookingSuccess,\n}: BookerProps) {\n const t = getBookerLabels(labels);\n const booker = useBooker({ initialData, target, timezone });\n\n if (booker.error) {\n return (\n \n );\n }\n\n const displayMeta = booker.meta;\n const selectedDate = booker.timePickerProps.selectedDate;\n const selectedTime = booker.confirmFormProps.selectedTime;\n const showSelectedSlot =\n booker.step === \"confirm\" && selectedDate != null && selectedTime != null;\n const headerImageAlt = displayMeta\n ? t.headerImageAlt(displayMeta.hostName, displayMeta.eventTypeTitle)\n : \"Booker header\";\n\n return (\n \n \n {/* Meta */}\n
\n \n \n
\n
\n \n {displayMeta ? (\n

\n {displayMeta.hostName}\n

\n ) : (\n \n )}\n
\n
\n {displayMeta ? (\n <>\n

\n {displayMeta.eventTypeTitle}\n

\n \n \n ) : (\n <>\n \n \n \n \n )}\n
\n
\n\n {displayMeta ? (\n
\n {showSelectedSlot ? (\n
\n \n \n {formatSelectedWeekday(\n selectedDate,\n booker.calendarProps.locale,\n )}{\" \"}\n {formatConfirmDateDetail(\n selectedDate,\n booker.calendarProps.locale,\n )}{\" \"}\n {formatConfirmTimeRange(\n selectedTime,\n booker.durationProps.value,\n !booker.timePickerProps.is24Hour,\n booker.calendarProps.locale,\n )}\n \n
\n ) : null}\n {displayMeta.eventTypeDurationOptions ? (\n \n ) : (\n
\n \n \n {displayMeta.eventTypeDurationMinutes\n ? t.durationMinutes(\n displayMeta.eventTypeDurationMinutes,\n )\n : t.durationUnknown}\n \n
\n )}\n \n {booker.step === \"select\" ? (\n \n ) : (\n \n )}\n
\n ) : (\n
\n \n \n \n
\n )}\n
\n \n \n {booker.step === \"select\" ? (\n
\n
\n \n
\n \n
\n ) : (\n \n \n \n )}\n
\n
\n \n Cal.com\n \n \n );\n}\n\nexport default Booker;\n", "type": "registry:block", "target": "components/atoms/booker-1.tsx" }, + { + "path": "registry/default/atoms/booker/booker-confirm-form.tsx", + "content": "\"use client\";\n\nimport { UserPlusIcon, XIcon } from \"lucide-react\";\nimport type { FormEvent } from \"react\";\nimport { useRef, useState } from \"react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Field, FieldError, FieldLabel } from \"@/registry/default/ui/field\";\nimport { Form } from \"@/registry/default/ui/form\";\nimport { Group } from \"@/registry/default/ui/group\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Textarea } from \"@/registry/default/ui/textarea\";\nimport type { BookerLabels } from \"./booker-labels\";\nimport { getLocationOptionValue, LocationPicker } from \"./location\";\nimport { createBookingAction } from \"@/lib/booker/actions\";\nimport type { BookerConfirmFormContext } from \"@/lib/booker/use-booker\";\nimport { toCalendarDateKey } from \"@/lib/booker/utils\";\nimport type { Booking } from \"@/lib/cal-api/types\";\n\nexport type BookerConfirmFormProps = BookerConfirmFormContext & {\n defaultEmail?: string;\n defaultName?: string;\n labels: BookerLabels;\n onSuccess?: (booking: Booking) => void;\n};\n\nfunction RequiredMark() {\n return *;\n}\n\nfunction getDefaultLocationProvider(\n locations: BookerConfirmFormContext[\"locations\"],\n): string {\n const location = locations?.[0];\n if (!location) {\n return \"\";\n }\n\n return location.provider || getLocationOptionValue(location, 0);\n}\n\nexport function BookerConfirmForm({\n defaultEmail,\n defaultName,\n disableGuests = false,\n durationMinutes,\n includeLengthInMinutes,\n labels,\n locale,\n locations = [],\n onBack,\n onSuccess,\n selectedDate,\n selectedTime,\n target,\n timeZone,\n}: BookerConfirmFormProps) {\n const nextGuestIdRef = useRef(0);\n const [guestIds, setGuestIds] = useState([]);\n const [locationProvider, setLocationProvider] = useState(() =>\n getDefaultLocationProvider(locations),\n );\n const [submitting, setSubmitting] = useState(false);\n const [submitError, setSubmitError] = useState(null);\n const [successBooking, setSuccessBooking] = useState(null);\n\n const resolvedLocationProvider =\n locations.length > 1\n ? locationProvider || getDefaultLocationProvider(locations)\n : getDefaultLocationProvider(locations);\n\n const handleAddGuest = () => {\n const id = nextGuestIdRef.current;\n nextGuestIdRef.current += 1;\n setGuestIds((current) => [...current, id]);\n };\n\n const handleRemoveGuest = (id: number) => {\n setGuestIds((current) => current.filter((guestId) => guestId !== id));\n };\n\n const handleSubmit = async (event: FormEvent) => {\n event.preventDefault();\n setSubmitError(null);\n\n if (!selectedDate || !selectedTime) {\n setSubmitError(labels.confirmError);\n return;\n }\n\n const formData = new FormData(event.currentTarget);\n const name = String(formData.get(\"name\") ?? \"\").trim();\n const email = String(formData.get(\"email\") ?? \"\").trim();\n const notes = String(formData.get(\"notes\") ?? \"\").trim();\n const guests = formData\n .getAll(\"guests\")\n .map((value) => String(value).trim())\n .filter(Boolean);\n\n if (!name || !email) {\n return;\n }\n\n setSubmitting(true);\n\n try {\n const result = await createBookingAction({\n email,\n guests: guests.length > 0 ? guests : undefined,\n ...(includeLengthInMinutes && durationMinutes\n ? { lengthInMinutes: durationMinutes }\n : {}),\n locale,\n locationProvider: resolvedLocationProvider || undefined,\n name,\n notes: notes || undefined,\n selectedDateKey: toCalendarDateKey(selectedDate),\n selectedTime,\n target,\n timeZone,\n });\n\n if (!result.ok) {\n setSubmitError(result.error || labels.confirmError);\n return;\n }\n\n setSuccessBooking(result.booking);\n onSuccess?.(result.booking);\n } catch {\n setSubmitError(labels.confirmError);\n } finally {\n setSubmitting(false);\n }\n };\n\n if (successBooking) {\n return (\n
\n

\n {labels.confirmSuccess}\n

\n {successBooking.meetingUrl ? (\n

\n \n {successBooking.meetingUrl}\n \n

\n ) : null}\n
\n );\n }\n\n return (\n
\n
\n \n \n {labels.confirmYourName} \n \n \n {labels.confirmNameError}\n \n\n \n \n {labels.confirmEmail} \n \n \n {labels.confirmEmailError}\n \n\n \n\n \n {labels.confirmNotes}\n