'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 { markOnboardingStepDone, finishOnboarding, skipOnboarding, updateMyProfile, } from '@/lib/actions' type StepType = 'welcome' | 'code_of_conduct' | 'profile' | 'payment' | 'content' interface Step { id: string step_type: StepType title: string body: string | null config: Record is_required: 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 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] 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) } 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" />