diff --git a/apps/shared/src/registrations/schemas.ts b/apps/shared/src/registrations/schemas.ts index d8e9ca4..fd064cc 100644 --- a/apps/shared/src/registrations/schemas.ts +++ b/apps/shared/src/registrations/schemas.ts @@ -1,10 +1,7 @@ import { z } from "zod"; import { RegistrationStatus } from "./constants.js"; -export const RegistrationAnswerSchema = z.record( - z.string(), - z.string().or(z.array(z.string())).optional() -); +export const RegistrationAnswerSchema = z.record(z.string(), z.string()); export const RegistrationContractSchema = z.object({ answers: RegistrationAnswerSchema.optional().default({}), diff --git a/apps/web/package.json b/apps/web/package.json index a570719..094c220 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "@clerk/tanstack-react-start": "^0.27.8", "@clerk/themes": "^2.4.46", "@events.comp-soc.com/shared": "workspace:*", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", diff --git a/apps/web/src/components/event-card.tsx b/apps/web/src/components/event-card.tsx index e23fe3e..64d2a80 100644 --- a/apps/web/src/components/event-card.tsx +++ b/apps/web/src/components/event-card.tsx @@ -2,25 +2,14 @@ import { Link } from '@tanstack/react-router' import { ArrowUpRight, CalendarIcon, MapPin } from 'lucide-react' import type { Event } from '@events.comp-soc.com/shared' import { SigBadge } from '@/components/sigs-badge.tsx' +import { formatEventDate } from '@/lib/utils.ts' interface EventCardProps { event: Event } function EventCard({ event }: EventCardProps) { - const dateObj = new Date(event.date) - - const formattedDate = dateObj.toLocaleDateString('en-GB', { - weekday: 'short', - day: 'numeric', - month: 'short', - }) - - const formattedTime = dateObj.toLocaleTimeString('en-GB', { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }) + const { full: date } = formatEventDate(event.date) return (
- - {formattedDate} - {formattedTime} - + {date}
diff --git a/apps/web/src/components/forms/event-registration-form-dialog.tsx b/apps/web/src/components/forms/event-registration-form-dialog.tsx new file mode 100644 index 0000000..8d40464 --- /dev/null +++ b/apps/web/src/components/forms/event-registration-form-dialog.tsx @@ -0,0 +1,278 @@ +import { useForm } from '@tanstack/react-form' + +import * as z from 'zod' + +import { RegistrationAnswerSchema } from '@events.comp-soc.com/shared' +import type { + CustomField, + RegistrationFormAnswer, +} from '@events.comp-soc.com/shared' + +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from '@/components/ui/field.tsx' +import { Input } from '@/components/ui/input.tsx' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select.tsx' +import { Textarea } from '@/components/ui/textarea.tsx' +import { Button } from '@/components/ui/button.tsx' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog.tsx' + +function buildRegistrationSchema(formStructure: Array) { + const schemaShape: Record = {} + + formStructure.forEach((field) => { + let fieldSchema: z.ZodTypeAny + + switch (field.type) { + case 'input': + case 'textarea': + fieldSchema = field.required + ? z.string().min(1, `${field.label} is required`) + : z.string().optional() + break + case 'select': + if (field.options && field.options.length > 0) { + fieldSchema = field.required + ? z.enum(field.options as [string, ...Array], { + error: () => ({ message: `${field.label} is required` }), + }) + : z.enum(field.options as [string, ...Array]).optional() + } else { + fieldSchema = z.string().optional() + } + break + default: + fieldSchema = z.string().optional() + } + + schemaShape[field.id] = fieldSchema + }) + + return z.object(schemaShape) +} + +function buildDefaultValues(formStructure: Array) { + const defaultValues: Record = {} + + formStructure.forEach((field) => { + defaultValues[field.id] = '' + }) + + return defaultValues +} + +function EventRegistrationFormDialog({ + onFormSubmit, + formStructure, + isLoading = false, + isOpen, + onOpenChange, + eventTitle, +}: { + onFormSubmit: (value: RegistrationFormAnswer) => void + formStructure: Array + isLoading?: boolean + isOpen: boolean + onOpenChange: () => void + eventTitle: string +}) { + const RegistrationSchema = buildRegistrationSchema(formStructure) + const defaultValues: z.infer = + buildDefaultValues(formStructure) + + const form = useForm({ + defaultValues: defaultValues, + validators: { + onSubmit: RegistrationSchema, + }, + onSubmit: ({ value }) => { + const mappedData = RegistrationAnswerSchema.parse(value) + onFormSubmit(mappedData) + }, + }) + + const renderField = (field: CustomField) => { + switch (field.type) { + case 'input': + return ( + { + const isInvalid = + formField.state.meta.isTouched && !formField.state.meta.isValid + return ( + + + {field.label} + {field.required && ( + * + )} + + formField.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder={`Enter ${field.label.toLowerCase()}`} + autoComplete="off" + /> + {isInvalid && ( + + )} + + ) + }} + /> + ) + + case 'textarea': + return ( + { + const isInvalid = + formField.state.meta.isTouched && !formField.state.meta.isValid + return ( + + + {field.label} + {field.required && ( + * + )} + +