'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { BrandMark } from '@/components/brand-mark' import { SafeMarkdown } from '@/components/safe-markdown' import { FormRenderer } from '@/components/forms/form-renderer' import type { FormField } from '@/lib/forms-schema' import { markOnboardingStepDone, finishOnboarding, skipOnboarding, updateMyProfile, submitForm, } from '@/lib/actions' type StepType = 'welcome' | 'code_of_conduct' | 'profile' | 'payment' | 'content' | 'form' interface StepFormDef { id: string title: string kind: string legalText: string | null fields: FormField[] } interface Step { id: string step_type: StepType title: string body: string | null config: Record is_required: boolean formDef?: StepFormDef | null alreadySubmitted?: boolean } interface Props { spaceName: string steps: Step[] canSkip: boolean profile: { display_name: string handle: string bio: string skills: string[] interests: string[] } } export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProfile }: Props) { const router = useRouter() const [index, setIndex] = useState(0) const [busy, setBusy] = useState(false) const [acked, setAcked] = useState>({}) const [profile, setProfile] = useState(initialProfile) // Controlled raw text for the skills field so edits survive navigating // Back/forward and the user sees exactly what they typed (no normalize). const [skillsText, setSkillsText] = useState(initialProfile.skills.join(', ')) const [formAnswers, setFormAnswers] = useState>>({}) const [formConsent, setFormConsent] = useState>({}) // Form steps already submitted in THIS session. Prevents a second insert if // the member navigates Back then Forward over a form/waiver step (the // server-side alreadySubmitted prop is a snapshot from page load). const [submittedNow, setSubmittedNow] = useState>({}) const step = steps[index] const isLast = index === steps.length - 1 const requireAck = step.config?.require_ack === true const ackLabel = (step.config?.ack_label as string) || 'I have read and agree' const ackOk = !requireAck || acked[step.id] const isFormStep = step.step_type === 'form' const formDef = step.formDef ?? null const effectiveSubmitted = step.alreadySubmitted === true || submittedNow[step.id] === true const formActionable = isFormStep && !!formDef && !effectiveSubmitted const isWaiver = formDef?.kind === 'waiver' const consentOk = !formActionable || !isWaiver || formConsent[step.id] === true async function advance() { setBusy(true) await markOnboardingStepDone(step.id) setBusy(false) if (isLast) { await finish() } else { setIndex(i => i + 1) } } async function finish() { setBusy(true) const result = await finishOnboarding() setBusy(false) if (result.error) { toast.error(result.error); return } router.push('/dashboard') } async function handleSkipAll() { setBusy(true) const result = await skipOnboarding() setBusy(false) if (result.error) { toast.error(result.error); return } router.push('/dashboard') } async function saveProfileAndAdvance() { setBusy(true) const result = await updateMyProfile({ display_name: profile.display_name.trim() || undefined, handle: profile.handle.trim() || null, bio: profile.bio.trim() || null, skills: profile.skills, interests: profile.interests, }) if ('error' in result && result.error) { setBusy(false); toast.error(result.error); return } await markOnboardingStepDone(step.id) setBusy(false) if (isLast) await finish() else setIndex(i => i + 1) } async function submitFormStep() { setBusy(true) // No form to fill (already done, or misconfigured/unpublished): treat as a // plain non-blocking step so a member is never trapped. if (formActionable && formDef) { const res = await submitForm({ formId: formDef.id, answers: formAnswers[step.id] ?? {}, consent: isWaiver ? formConsent[step.id] === true : undefined, }) if ('error' in res && res.error) { setBusy(false) toast.error(res.error) return } setSubmittedNow(p => ({ ...p, [step.id]: true })) } await markOnboardingStepDone(step.id) setBusy(false) if (isLast) await finish() else setIndex(i => i + 1) } const progressPct = Math.round(((index) / steps.length) * 100) return (
{spaceName} {canSkip && ( )}

Step {index + 1} of {steps.length}

{step.is_required && ( required )}

{step.title}

{step.step_type === 'profile' ? (
{step.body && {step.body}}
setProfile({ ...profile, display_name: e.target.value })} maxLength={100} className="w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary" />
setProfile({ ...profile, handle: e.target.value })} maxLength={50} placeholder="optional" className="w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary" />