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 && (
+ *
+ )}
+
+
+ )
+ }}
+ />
+ )
+
+ case 'select':
+ return (
+ {
+ const isInvalid =
+ formField.state.meta.isTouched && !formField.state.meta.isValid
+ return (
+
+
+ {field.label}
+ {field.required && (
+ *
+ )}
+
+
+ {isInvalid && (
+
+ )}
+
+ )
+ }}
+ />
+ )
+
+ default:
+ return null
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default EventRegistrationFormDialog
diff --git a/apps/web/src/components/forms/modify-event-form.tsx b/apps/web/src/components/forms/modify-event-form.tsx
index a7a2801..0450fc4 100644
--- a/apps/web/src/components/forms/modify-event-form.tsx
+++ b/apps/web/src/components/forms/modify-event-form.tsx
@@ -111,10 +111,12 @@ function ModifyEventForm({
onFormSubmit,
defaultValues,
isModify = false,
+ isLoading = false,
}: {
onFormSubmit: (value: z.infer) => void
defaultValues: z.infer
isModify?: boolean
+ isLoading?: boolean
}) {
const [open, setOpen] = useState(false)
@@ -248,6 +250,7 @@ function ModifyEventForm({
Title
Organiser
@@ -145,9 +112,13 @@ function EventRoute() {
{isDraft ? (
-
+
) : (
-
+
)}
{isCommittee && (