import React, { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { formsApi } from "../services/api"; import type { FormField, PublicFormDefinition, PublicFormEventSummary } from "../services/api"; import type { FormEventHeaderDisplay } from "../app/utils/formEventHeaderDisplay"; import { assignUniqueFieldNames, fieldDataKey, fieldDisplayLabel, countryStateKeys, isCountryStateField } from "../app/utils/formFieldNames"; import { SelectWithOtherInput } from "../app/components/forms/SelectWithOtherInput"; import { ensureRegistrationCoreFields } from "../app/utils/registrationFormFields"; import { COUNTRY_NAMES, DEFAULT_COUNTRY, statesForCountry } from "../app/data/countryStates"; const EVENT_TYPE_LABEL: Record = { conference: "Conference", workshop: "Workshop", summit: "Summit", cme: "CME", webinar: "Webinar", }; function formatEventType(t?: string): string { if (!t) return ""; return EVENT_TYPE_LABEL[t] || t.replace(/^\w/, (c) => c.toUpperCase()); } function formatLocationKind(locationType?: string): string { if (locationType === "virtual") return "Virtual"; if (locationType === "hybrid") return "Hybrid"; return "In person"; } function formatEventDateRange(ev: PublicFormEventSummary): string { const { startDate, endDate } = ev; if (!startDate) return "—"; const s = new Date(startDate); const e = endDate ? new Date(endDate) : s; if (Number.isNaN(s.getTime())) return "—"; const sameDay = !Number.isNaN(e.getTime()) && s.toDateString() === e.toDateString(); const dOpt: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric" }; if (sameDay) { return s.toLocaleDateString(undefined, { weekday: "short", ...dOpt }); } return `${s.toLocaleDateString(undefined, dOpt)} – ${e.toLocaleDateString(undefined, dOpt)}`; } function shouldShowLocation(ev: PublicFormEventSummary): boolean { const loc = ev.location?.trim(); if (!loc) return false; return ev.locationType === "physical" || ev.locationType === "hybrid"; } function PublicFormEventHeader({ event, variant = "default", display, }: { event: PublicFormEventSummary; variant?: "default" | "embedded"; display?: FormEventHeaderDisplay | null; }) { const d = { showLogo: display?.showLogo !== false, showTitle: display?.showTitle !== false, showDateRange: display?.showDateRange !== false, showEventType: display?.showEventType !== false, showLocationType: display?.showLocationType !== false, showVenue: display?.showVenue !== false, }; const typeLabel = formatEventType(event.eventType); const dateLine = formatEventDateRange(event); const kind = formatLocationKind(event.locationType); const showLoc = d.showVenue && shouldShowLocation(event); const embedded = variant === "embedded"; const showBanner = d.showLogo && !!event.imageUrl && !event.logoUrl; const showLogo = d.showLogo && !!event.logoUrl; const showTitle = d.showTitle && !!event.name; const showSubtitle = d.showTitle && !!event.subtitle?.trim(); const metaItems: Array<{ key: string; node: React.ReactNode }> = []; if (d.showDateRange && dateLine) { metaItems.push({ key: "date", node: {dateLine}, }); } if (d.showEventType && typeLabel) { metaItems.push({ key: "type", node: {typeLabel} }); } if (d.showLocationType && kind) { metaItems.push({ key: "kind", node: ( {kind} ), }); } if (showLoc) { metaItems.push({ key: "venue", node: {event.location!.trim()}, }); } if (!showBanner && !showLogo && !showTitle && !showSubtitle && metaItems.length === 0) { return null; } return (
{showBanner ? (
) : null}
{showLogo ? ( {event.name} ) : null}
{showTitle ? (

{event.name}

) : null} {showSubtitle ? (

{event.subtitle}

) : null} {metaItems.length > 0 ? (
{metaItems.map((item, index) => ( {index > 0 ? ( ) : null} {item.node} ))}
) : null}
); } function fieldValueToData( field: FormField, values: Record ): Record { if (field.type === "section") return {}; if (isCountryStateField(field)) { const { country, state } = countryStateKeys(field); return { [country]: values[country] ?? "", [state]: values[state] ?? "" }; } const k = fieldDataKey(field); if (!k) return {}; const value = values[k]; if (field.type === "checkboxes" && Array.isArray(value)) { return { [k]: value }; } if (field.type === "checkbox") { return { [k]: value === true || value === "true" ? "Yes" : "" }; } return { [k]: value }; } /** `stars` type or legacy `select` with options "1"…"5" (form builder star rating). */ function isStarRatingField(field: FormField): boolean { if (field.type === "stars") return true; if (field.type !== "select" || !field.options?.length) return false; const o = field.options.map((x) => String(x)); return o.length === 5 && o.every((v, i) => v === String(i + 1)); } function starRatingMax(field: FormField): number { if (field.type === "stars") { const n = field.options?.length; return n && n > 0 ? n : 5; } return 5; } function FieldRemarks({ text }: { text?: string }) { const trimmed = text?.trim(); if (!trimmed) return null; return (

{trimmed}

); } export function PublicFormView() { const { formId = "" } = useParams(); const [form, setForm] = useState(null); const [loadError, setLoadError] = useState(null); const [values, setValues] = useState>({}); const [fileNames, setFileNames] = useState>({}); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const [done, setDone] = useState(false); const load = useCallback(async () => { if (!formId) return; setLoadError(null); try { const f = await formsApi.getPublic(formId); const rawFields = f.type === "registration" ? ensureRegistrationCoreFields(f.fields || []) : f.fields || []; setForm({ ...f, fields: assignUniqueFieldNames(rawFields) }); } catch (e) { setLoadError(e instanceof Error ? e.message : "Could not load form."); } }, [formId]); useEffect(() => { void load(); }, [load]); const setField = (key: string, v: unknown) => { setValues((prev) => ({ ...prev, [key]: v })); }; const validate = useCallback((): string | null => { if (!form?.fields?.length) return null; for (const field of form.fields) { if (field.type === "section") continue; const display = fieldDisplayLabel(field); if (!field.required) continue; if (isCountryStateField(field)) { const { country, state } = countryStateKeys(field); const countryVal = values[country]; const stateVal = values[state]; if (countryVal == null || (typeof countryVal === "string" && countryVal.trim() === "")) { return `“${display}” country is required.`; } const stateOptions = statesForCountry(String(countryVal)); if ( stateOptions.length > 0 && (stateVal == null || (typeof stateVal === "string" && stateVal.trim() === "")) ) { return `“${display}” state is required.`; } continue; } const k = fieldDataKey(field); if (!k) continue; const t = field.type; if (t === "checkboxes") { const arr = values[k]; if (!Array.isArray(arr) || arr.length === 0) return `“${display}” is required.`; continue; } if (t === "checkbox") { if (!values[k]) return `“${display}” is required.`; continue; } const v = values[k]; if (v == null || (typeof v === "string" && v.trim() === "")) { return `“${display}” is required.`; } } return null; }, [form, values]); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formId || !form) return; setSubmitError(null); const err = validate(); if (err) { setSubmitError(err); return; } const data: Record = {}; for (const field of form.fields || []) { if (field.type === "section") continue; Object.assign(data, fieldValueToData(field, values)); } setSubmitting(true); try { await formsApi.submit(formId, { data }); setDone(true); } catch (e) { setSubmitError( e instanceof Error ? e.message : form.settings?.submitErrorMessage?.trim() || "Submit failed.", ); } finally { setSubmitting(false); } }; if (loadError) { return (

{loadError}

); } if (!form) { return (
Loading…
); } if (done) { const showThanks = form.settings?.showThankYou !== false; const msg = form.settings?.thankYouMessage?.trim() || "Thank you for your response."; const ev = form.event; return (
{ev ? : null}

{form.name}

{showThanks ? (

{msg}

) : null}
); } const labelStyle: React.CSSProperties = { display: "block", fontSize: "0.75rem", fontWeight: 600, color: "var(--muted, #64748b)", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.04em", }; const inputStyle: React.CSSProperties = { width: "100%", padding: "10px 12px", fontSize: "0.9rem", borderRadius: 8, border: "1px solid var(--border, #e2e8f0)", background: "var(--card, #fff)", color: "var(--foreground, #0f172a)", boxSizing: "border-box", }; const ev = form.event; return (
{ev ? : null}

{form.name}

{form.description ? (

{form.description}

) : (
)}
void onSubmit(e)} style={{ padding: 24, background: "var(--card, #fff)", border: "1px solid var(--border, #e2e8f0)", borderRadius: "var(--radius-card, 12px)", display: "flex", flexDirection: "column", gap: 16, boxShadow: "0 1px 3px rgba(0,0,0,0.06)", }} > {(form.fields || []).map((field, index) => { if (field.type === "section") { const title = (field.label || "").trim(); if (!title) return null; return (
{index > 0 ? (
) : null}

{title}

); } const k = fieldDataKey(field); const display = fieldDisplayLabel(field); if (!k) return null; const req = field.required; if (field.type === "textarea") { return (