import { ReactNode, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { ArrowLeft, ArrowRight, Check, ChevronLeft } from "lucide-react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; /** * Generic multi-step wizard shell. * * A wizard is defined as an ordered array of {@link WizardStepDef} items. * Each step owns its own piece of the accumulated state and returns a node * that renders the form. The shell handles: * * - rendering the numbered stepper (with `completed` / `active` styles) * - Back / Next navigation * - a final "Finish" button that calls `onFinish(state)` * - per-step validation via `step.validate(state)` → return an error * message or `null` when valid * - a persistent progress bar * * Keeping this shell free of domain logic means every scenario wizard * (rubric, course, structure, …) is just a tiny file that defines steps * and wires the final submit to the right service. */ export interface WizardStepDef { id: string; /** i18n key for the step title. */ titleKey: string; /** Optional i18n key for a short description shown under the title. */ descriptionKey?: string; /** * Render the step's form. Call `update(patch)` to merge fields into the * shared state. Do **not** call `onNext` directly — the shell handles * Next/Back; the render function should just update local fields. */ render: (props: StepRenderProps) => ReactNode; /** * Synchronous validator. Return a human-readable error message that will * be displayed under the form and block Next, or `null` when valid. */ validate?: (state: TState) => string | null; } export interface StepRenderProps { state: TState; update: (patch: Partial) => void; error: string | null; } export interface StepWizardProps { /** i18n key for the wizard heading. */ titleKey: string; /** i18n key for the short lead paragraph shown under the heading. */ subtitleKey?: string; /** Optional back-link to the hub. */ backTo?: string; backLabelKey?: string; steps: WizardStepDef[]; initialState: TState; /** Called once the user clicks Finish on the last step. */ onFinish: (state: TState) => Promise | void; /** i18n key for the Finish button label. Defaults to "wizard.finish". */ finishLabelKey?: string; /** Disable all form controls (used by consumers while submitting). */ submitting?: boolean; } export function StepWizard({ titleKey, subtitleKey, backTo, backLabelKey = "wizard.backToHub", steps, initialState, onFinish, finishLabelKey = "wizard.finish", submitting = false, }: StepWizardProps) { const { t } = useTranslation(); const [index, setIndex] = useState(0); const [state, setState] = useState(initialState); const [error, setError] = useState(null); const [submittingInternal, setSubmittingInternal] = useState(false); const busy = submitting || submittingInternal; const current = steps[index]; const isLast = index === steps.length - 1; const progress = Math.round(((index + 1) / steps.length) * 100); const update = (patch: Partial) => { setState((prev) => ({ ...prev, ...patch })); // Clear validation error as soon as the user starts typing again. if (error) setError(null); }; const validateAndAdvance = async () => { const msg = current.validate?.(state) ?? null; if (msg) { setError(msg); return; } setError(null); if (!isLast) { setIndex((i) => i + 1); return; } // Last step: submit. try { setSubmittingInternal(true); await onFinish(state); } finally { setSubmittingInternal(false); } }; const stepStatus = useMemo( () => steps.map((_, i) => { if (i < index) return "done" as const; if (i === index) return "active" as const; return "upcoming" as const; }), [steps, index], ); return (
{/* Heading + back link */}
{backTo && ( )}

{t(titleKey)}

{subtitleKey &&

{t(subtitleKey)}

}
{/* Stepper */}
    {steps.map((step, i) => { const s = stepStatus[i]; return (
  1. {s === "done" ? : i + 1}
    {t(step.titleKey)} {i < steps.length - 1 &&
    }
  2. ); })}
{/* Progress bar */}
{/* Active step */}

{t(current.titleKey)}

{current.descriptionKey && (

{t(current.descriptionKey)}

)}
{current.render({ state, update, error })} {error && (
{error}
)}
{/* Navigation */}
{t("wizard.stepOf", { current: index + 1, total: steps.length })}
); } export default StepWizard;