From 0a8788afff0b56c77189bd5d62b6b7595873d90a Mon Sep 17 00:00:00 2001 From: joshua Date: Wed, 21 Oct 2026 10:42:17 +0200 Subject: [PATCH] Type supervisor forms, manual registration and events pages Co-Authored-By: Claude Opus 5.5 --- .../app/dashboard/supervisor/events/page.tsx | 156 +++++++++++------- .../app/dashboard/supervisor/forms/page.tsx | 117 ++++++------- .../app/dashboard/supervisor/manual/page.tsx | 83 +++++----- frontend/src/types/index.ts | 11 +- 4 files changed, 204 insertions(+), 163 deletions(-) diff --git a/frontend/src/app/dashboard/supervisor/events/page.tsx b/frontend/src/app/dashboard/supervisor/events/page.tsx index be443f8..a2b88ec 100644 --- a/frontend/src/app/dashboard/supervisor/events/page.tsx +++ b/frontend/src/app/dashboard/supervisor/events/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; @@ -11,6 +11,7 @@ import { useSiteSettings } from "@/contexts/SiteSettingsContext"; import { Calendar } from "lucide-react"; import { ChoicesEditor } from "@/components/shared/FormFieldExtras"; import { errorMessage } from "@/lib/utils"; +import type { Event, EventAttachment, EventFormField, EventOption, Paginated, Section } from "@/types"; // ─── helpers ──────────────────────────────────────────────────────────────── @@ -46,7 +47,26 @@ function DTInput({ label, value, onChange, hint, required }: { label: string; va // ─── types ─────────────────────────────────────────────────────────────────── type FormFieldType = "yes_no" | "text" | "date" | "numeric" | "statement" | "paragraph" | "rating" | "multiple_choice"; -type EventFormDef = { isRequired: boolean; fields: { id?: string; type: FormFieldType; label: string; isRequired?: boolean; order?: number; helpText?: string | null; options?: any }[] }; +type EventFormDef = { isRequired: boolean; fields: { id?: string; type: FormFieldType; label: string; isRequired?: boolean; order?: number; helpText?: string | null; options?: unknown }[] }; +type FieldDef = EventFormDef["fields"][number]; + +// What PUT /api/events/options/:id takes as earlyBirdTiers (variantId null = option-level). +type TierPayload = { id?: string; deadline: string; price: number; stockLimit: number; order: number; variantId: string | null }; + +// GET /api/feedback/event/:id — the event's post-event feedback form, if it has one. +type FeedbackFormResponse = { + fields?: EventFormField[]; + sendAt?: string | null; + audience?: string; + anonymous?: boolean; + intro?: string | null; + sentAt: string | null; + invited: number; + responses: number; +}; + +const toFieldDefs = (fields: EventFormField[] | undefined): FieldDef[] => + (fields || []).map((x) => ({ id: x.id, type: x.type, label: x.label, isRequired: !!x.isRequired, helpText: x.helpText || "", options: x.options || undefined })); type Tier = { id?: string; deadline: string; price: string; stockLimit: string; order?: number }; @@ -90,8 +110,9 @@ function UploadImageButton({ onUploaded, label = "Upload image" }: { onUploaded: try { const fd = new FormData(); fd.append("image", file); - const res: any = await apiFetch("/api/uploads/event-image", { method: "POST", body: fd, authToken: token }); - if (res?.url || res?.path) onUploaded(res.url || res.path); + const res = await apiFetch<{ url?: string; path?: string }>("/api/uploads/event-image", { method: "POST", body: fd, authToken: token }); + const uploaded = res?.url || res?.path; + if (uploaded) onUploaded(uploaded); } finally { setUploading(false); if (ref.current) ref.current.value = ""; } }} />
- upd(idx, { type: e.target.value as FormFieldType })}> @@ -454,24 +475,24 @@ function SectionsDraftEditor({ sections, options, onChange }: { /** Edit-mode: live sections manager (DB-backed) */ function SectionsManager({ eventId }: { eventId: string }) { const { token } = useAuth(); - const [sections, setSections] = useState([]); - const [eventOptions, setEventOptions] = useState([]); + const [sections, setSections] = useState([]); + const [eventOptions, setEventOptions] = useState([]); const [loading, setLoading] = useState(false); const [newName, setNewName] = useState(""); const [newOptionIds, setNewOptionIds] = useState([]); - const load = async () => { + const load = useCallback(async () => { setLoading(true); try { const [secList, evFull] = await Promise.all([ - apiFetch("/api/sections", { authToken: token || undefined }), - apiFetch(`/api/events/${eventId}`) + apiFetch("/api/sections", { authToken: token || undefined }), + apiFetch(`/api/events/${eventId}`) ]); setSections((secList || []).filter(s => s.eventId === eventId)); setEventOptions(evFull?.eventOptions || []); } catch {} finally { setLoading(false); } - }; - useEffect(() => { if (eventId) load(); }, [eventId]); + }, [eventId, token]); + useEffect(() => { if (eventId) load(); }, [eventId, load]); const create = async () => { if (!newName.trim() || !token) return; @@ -516,14 +537,19 @@ function SectionsManager({ eventId }: { eventId: string }) { ); } -function SectionRow({ sec, options, onDelete, onUpdate }: any) { +function SectionRow({ sec, options, onDelete, onUpdate }: { + sec: Section; + options: EventOption[]; + onDelete: () => void; + onUpdate: (id: string, name: string, optionIds: string[]) => void; +}) { const [name, setName] = useState(sec.name || ""); - const [optIds, setOptIds] = useState((sec.allowedOptions || []).map((o: any) => o.eventOptionId)); + const [optIds, setOptIds] = useState((sec.allowedOptions || []).map((o) => o.eventOptionId)); return (
setName(e.target.value)} />
- {options.map((opt: any) => ( + {options.map((opt) => (