diff --git a/src/components/form/FormWizard.tsx b/src/components/form/FormWizard.tsx deleted file mode 100644 index 711ae586e..000000000 --- a/src/components/form/FormWizard.tsx +++ /dev/null @@ -1,18 +0,0 @@ -type WizardStepObjectBase = { - label: string; - hidden?: boolean; -}; - -export type WizardStepObject = WizardStepObjectBase & - ( - | { - id: string | number; - variant?: 'body'; - fields: (keyof Fields)[]; - } - | { - id?: never; - variant: 'start' | 'finish'; - fields?: never; - } - ); diff --git a/src/components/form/FormWizard/FormWizard.tsx b/src/components/form/FormWizard/FormWizard.tsx new file mode 100644 index 000000000..eb515e2ad --- /dev/null +++ b/src/components/form/FormWizard/FormWizard.tsx @@ -0,0 +1,409 @@ +'use client'; + +import Button from '@mui/material/Button'; +import Slide from '@mui/material/Slide'; +import Step from '@mui/material/Step'; +import StepButton from '@mui/material/StepButton'; +import StepLabel from '@mui/material/StepLabel'; +import Stepper from '@mui/material/Stepper'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { + Children, + isValidElement, + MouseEvent, + ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { BaseCard } from '@src/components/common/BaseCard'; +import Panel from '@src/components/common/Panel'; +import { useFormContext } from '@src/utils/form'; +import { + ActiveStep, + FormWizardProps, + FormWizardStepProps, + StepConfig, +} from './types'; +import { useWizardContext, WizardContext } from './WizardContext'; + +/** + * Reusable multi-step form wizard that integrates with TanStack Form. + * + * Usage: + * ```tsx + * router.push('/')}> + * + * + * + * + * ...form fields... + * + * + * + * + * + * ``` + */ +export default function FormWizard({ + onComplete, + autoAdvanceOnSubmit, + children, +}: FormWizardProps) { + const form = useFormContext(); + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); + + // Build step config from children + const steps = useMemo(() => { + const result: StepConfig[] = []; + + Children.toArray(children).forEach((child) => { + if ( + isValidElement(child) && + typeof child.type !== 'string' && + '_isWizardStep' in child.type + ) { + const props = child.props as FormWizardStepProps & { + children: ReactNode; + }; + + if (props.startStep) { + result.push({ + label: props.label ?? 'Get Started', + content: props.children, + variant: 'start', + hidden: true, + }); + } else if (props.finishStep) { + result.push({ + label: props.label ?? 'Finish', + content: props.children, + variant: 'finish', + hidden: true, + }); + } else { + result.push({ + label: props.label ?? '', + fields: props.fields ?? [], + content: props.children, + variant: 'body', + hidden: false, + }); + } + } + }); + + return result; + }, [children]); + + const hasStart = steps[0]?.variant === 'start'; + const hasFinish = steps[steps.length - 1]?.variant === 'finish'; + const shouldAutoAdvance = autoAdvanceOnSubmit ?? hasFinish; + + // Step navigation state + const [activeStep, setActiveStep] = useState({ + index: 0, + previous: undefined, + }); + + // Loading state to prevent flash before first render measurement + const [mounting, setMounting] = useState(true); + + // Dynamic height for absolutely-positioned step content + const [formHeight, setFormHeight] = useState(0); + + const observerRef = useRef(null); + + const measureFormStepRef = useCallback((node: HTMLDivElement | null) => { + // Disconnect previous observer when ref detaches (React calls with null) + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; + } + + if (node !== null) { + setMounting(false); + setFormHeight(node.clientHeight); + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) setFormHeight(entry.contentRect.height); + }); + observer.observe(node); + observerRef.current = observer; + } + }, []); + + // Validation helpers + const validateStepFields = useCallback( + (stepIndex: number) => { + const step = steps[stepIndex]; + if (step?.variant !== 'body' || !step.fields.length) return; + step.fields.forEach((field) => + form.validateField(field as never, 'change'), + ); + }, + [steps, form], + ); + + const areStepFieldsValid = useCallback( + (stepIndex: number) => { + const step = steps[stepIndex]; + if (step?.variant !== 'body' || !step.fields.length) return true; + return step.fields.every( + (field) => + ( + form.state.fieldMeta as Record< + string, + { isValid?: boolean } | undefined + > + )[field]?.isValid ?? true, + ); + }, + [steps, form.state.fieldMeta], + ); + + // Last body step index (the step that triggers form submission) + const lastBodyIndex = hasFinish ? steps.length - 2 : steps.length - 1; + + // Navigation + const goNext = useCallback(() => { + validateStepFields(activeStep.index); + if (!areStepFieldsValid(activeStep.index)) return; + + if (activeStep.index < lastBodyIndex) { + setActiveStep((prev) => ({ + index: prev.index + 1, + previous: prev.index, + })); + } else if (hasFinish && activeStep.index === steps.length - 1) { + onComplete?.(); + } + }, [ + activeStep, + lastBodyIndex, + hasFinish, + steps.length, + onComplete, + validateStepFields, + areStepFieldsValid, + ]); + + const goBack = useCallback(() => { + if (activeStep.index > 0) { + setActiveStep((prev) => ({ + index: prev.index - 1, + previous: prev.index, + })); + } + }, [activeStep]); + + const goToStep = useCallback( + (index: number) => { + if (index < activeStep.index) { + setActiveStep((prev) => ({ + index: index, + previous: prev.index, + })); + } else if (index > activeStep.index) { + validateStepFields(activeStep.index); + if (!areStepFieldsValid(activeStep.index)) return; + if (index - 1 > activeStep.index) return; + setActiveStep((prev) => ({ + index: index, + previous: prev.index, + })); + } + }, + [activeStep, validateStepFields, areStepFieldsValid], + ); + + // Advance to finish step after successful form submission + const goToFinish = useCallback(() => { + if (hasFinish) { + setActiveStep((prev) => ({ + index: steps.length - 1, + previous: prev.index, + })); + } + }, [hasFinish, steps.length]); + + const handleNext = (event: MouseEvent) => { + // Always prevent native submit; we call form.handleSubmit() explicitly + event.preventDefault(); + + validateStepFields(activeStep.index); + if (!areStepFieldsValid(activeStep.index)) return; + + if (activeStep.index < lastBodyIndex) { + setActiveStep((prev) => ({ + index: prev.index + 1, + previous: prev.index, + })); + } else if (activeStep.index === lastBodyIndex) { + // Submit the form; only advance to the finish step once the API call + // resolves successfully so the step does not jump early + void form.handleSubmit().then(() => { + if ( + form.store.state.isSubmitSuccessful && + shouldAutoAdvance && + hasFinish + ) { + setActiveStep({ + index: steps.length - 1, + previous: activeStep.index, + }); + } + }); + } else if (hasFinish && activeStep.index === steps.length - 1) { + // "Continue" button on finish screen + onComplete?.(); + } + }; + + // Context value + const contextValue = useMemo( + () => ({ + activeStep, + steps, + goNext, + goBack, + goToStep, + goToFinish, + }), + [activeStep, steps, goNext, goBack, goToStep, goToFinish], + ); + + // Button labels and states + const isOnFinishStep = hasFinish && activeStep.index === steps.length - 1; + const isOnLastBodyStep = activeStep.index === lastBodyIndex; + const isOnStartStep = hasStart && activeStep.index === 0; + + const nextButtonLabel = isOnFinishStep + ? 'Continue' + : isOnLastBodyStep + ? 'Submit' + : isOnStartStep + ? 'Start' + : 'Next'; + + const currentFieldsValid = areStepFieldsValid(activeStep.index); + + return ( + +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="flex flex-col gap-8 w-full" + noValidate + > + +
+ + {steps.map((step, index) => { + if (!step.hidden) { + return ( + + goToStep(index)} + disabled={ + index - 1 > activeStep.index || isOnFinishStep + } + > + + {step.label} + + + + ); + } + })} + +
+
+ +
+ {/* Hidden step for initial sizing */} +
+
{steps[0]?.content}
+
+ + {steps.map((step, index) => { + const isActive = activeStep.index === index; + const direction = + activeStep.previous !== undefined + ? activeStep.index > activeStep.previous + ? activeStep.index === index + ? 'left' + : 'right' + : activeStep.index === index + ? 'right' + : 'left' + : 'left'; + + return ( + +
+
{step.content}
+
+
+ ); + })} +
+
+ + +
+
+
+
+ ); +} + +// Re-export for consumers that need wizard context +export { useWizardContext }; diff --git a/src/components/form/FormWizard/FormWizardStep.tsx b/src/components/form/FormWizard/FormWizardStep.tsx new file mode 100644 index 000000000..4af7c434c --- /dev/null +++ b/src/components/form/FormWizard/FormWizardStep.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { ReactNode } from 'react'; +import { FormWizardStepProps } from './types'; + +type WizardStepComponent = ((props: FormWizardStepProps) => ReactNode) & { + _isWizardStep: true; +}; + +/** + * Declarative step component for FormWizard. + * FormWizard reads its props via React.Children to build step configuration. + * The component itself simply renders its children when active. + */ +const FormWizardStep: WizardStepComponent = ({ children }) => { + return <>{children}; +}; + +FormWizardStep._isWizardStep = true; + +export default FormWizardStep; diff --git a/src/components/form/FormWizard/WizardContext.tsx b/src/components/form/FormWizard/WizardContext.tsx new file mode 100644 index 000000000..b561467cb --- /dev/null +++ b/src/components/form/FormWizard/WizardContext.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { createContext, useContext } from 'react'; +import { WizardContextType } from './types'; + +const defaultContext: WizardContextType = { + activeStep: { index: 0, previous: undefined }, + steps: [], + goNext: () => {}, + goBack: () => {}, + goToStep: () => {}, + goToFinish: () => {}, +}; + +export const WizardContext = createContext(defaultContext); + +export function useWizardContext() { + return useContext(WizardContext); +} diff --git a/src/components/form/FormWizard/index.ts b/src/components/form/FormWizard/index.ts new file mode 100644 index 000000000..2d5ef4260 --- /dev/null +++ b/src/components/form/FormWizard/index.ts @@ -0,0 +1,8 @@ +export { default as FormWizard } from './FormWizard'; +export { useWizardContext } from './WizardContext'; +export { default as FormWizardStep } from './FormWizardStep'; +export type { + WizardStepObject, + FormWizardProps, + FormWizardStepProps, +} from './types'; diff --git a/src/components/form/FormWizard/types.ts b/src/components/form/FormWizard/types.ts new file mode 100644 index 000000000..6b14ed278 --- /dev/null +++ b/src/components/form/FormWizard/types.ts @@ -0,0 +1,69 @@ +import { ReactNode } from 'react'; + +type WizardStepObjectBase = { + label: string; + hidden?: boolean; +}; + +export type WizardStepObject = WizardStepObjectBase & + ( + | { + id: string | number; + variant?: 'body'; + fields: (keyof Fields)[]; + } + | { + id?: never; + variant: 'start' | 'finish'; + fields?: never; + } + ); + +export type FormWizardStepProps = { + /** Label shown in the stepper (required for body steps) */ + label?: string; + /** Field names belonging to this step (used for validation) */ + fields?: string[]; + /** Step content */ + children: ReactNode; + /** Marks this step as the intro screen (hidden from stepper, placed first) */ + startStep?: boolean; + /** Marks this step as the completion screen (hidden from stepper, placed last) */ + finishStep?: boolean; +}; + +export type FormWizardProps = { + /** Called when the user clicks "Continue" on the finish step */ + onComplete?: () => void; + /** + * If true, automatically advances to the finish step after successful + * form submission. Defaults to true when a finishStep child is present. + */ + autoAdvanceOnSubmit?: boolean; + /** Wizard step children */ + children: ReactNode; +}; + +type StepConfigBase = { + label: string; + content: ReactNode; + hidden: boolean; +}; + +export type StepConfig = + | (StepConfigBase & { variant: 'body'; fields: string[] }) + | (StepConfigBase & { variant: 'start' | 'finish' }); + +export type ActiveStep = { + index: number; + previous: number | undefined; +}; + +export type WizardContextType = { + activeStep: ActiveStep; + steps: StepConfig[]; + goNext: () => void; + goBack: () => void; + goToStep: (index: number) => void; + goToFinish: () => void; +}; diff --git a/src/components/getting-started/OnboardingForm.tsx b/src/components/getting-started/OnboardingForm.tsx index 7bbf29456..4e4294a79 100644 --- a/src/components/getting-started/OnboardingForm.tsx +++ b/src/components/getting-started/OnboardingForm.tsx @@ -1,57 +1,23 @@ 'use client'; -import Button from '@mui/material/Button'; -import Slide from '@mui/material/Slide'; -import Step from '@mui/material/Step'; -import StepButton from '@mui/material/StepButton'; -import StepLabel from '@mui/material/StepLabel'; -import Stepper from '@mui/material/Stepper'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/system/useMediaQuery'; +import Typography from '@mui/material/Typography'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { useMutation } from '@tanstack/react-query'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { MouseEvent, useCallback, useState } from 'react'; -import { BaseCard } from '@src/components/common/BaseCard'; -import Panel from '@src/components/common/Panel'; -import { WizardStepObject } from '@src/components/form/FormWizard'; +import { ReactNode, useState } from 'react'; +import { majors, minors } from '@src/constants/utdDegrees'; import { SelectUserMetadataWithClubs } from '@src/server/db/models'; +import { studentClassificationEnum } from '@src/server/db/schema/users'; import { useTRPC } from '@src/trpc/react'; import { useAppForm } from '@src/utils/form'; import { accountOnboardingSchema, AccountOnboardingSchema, } from '@src/utils/formSchemas'; -import OnboardingFormStep from './OnboardingFormStep'; - -// "Source of truth" array that contains the actual steps of the form -const stepsBody = [ - { id: 1, label: 'Name', fields: ['firstName', 'lastName'] }, - { - id: 2, - label: 'College Info', - fields: ['major', 'minor', 'studentClassification', 'graduationDate'], - }, - { id: 3, label: 'Contact Email', fields: ['contactEmail'] }, -] as const satisfies readonly WizardStepObject[]; - -// Extracts a union of all the ids used in rawSteps -export type stepIds = (typeof stepsBody)[number]['id']; - -// Creates the generic array of WizardStepObjects -export const steps: readonly WizardStepObject[] = [ - { variant: 'start', label: 'Get Started', hidden: true }, - ...stepsBody, - { variant: 'finish', label: 'Finish', hidden: true }, -]; - -const hasStart = steps.find((step) => step.variant === 'start'); -const hasFinish = steps.find((step) => step.variant === 'finish'); type OnboardingFormProps = { userMetadata?: SelectUserMetadataWithClubs; - /** - * Include div's for centering and keeping content within a max width - */ withLayout?: boolean; }; @@ -60,9 +26,6 @@ export default function OnboardingForm({ withLayout = false, }: OnboardingFormProps) { const router = useRouter(); - - const theme = useTheme(); - const api = useTRPC(); const editAccountMutation = useMutation( @@ -77,8 +40,6 @@ export default function OnboardingForm({ major: userMetadata?.major, minor: userMetadata?.minor, studentClassification: userMetadata?.studentClassification, - // `userMetadata.graduation` is automatically set with a time zone, which shows the wrong month in the date picker - // Add the timezone offset (in milliseconds) to convert back to UTC graduationDate: userMetadata?.graduationDate ? new Date( userMetadata?.graduationDate?.getTime() + @@ -105,12 +66,6 @@ export default function OnboardingForm({ ) : null, }; - - setActiveStep((prev) => ({ - current: steps.length - 1, - previous: prev.current, - })); - setDefaultValues(updatedFixed); formApi.reset(updatedFixed); } @@ -118,283 +73,196 @@ export default function OnboardingForm({ console.error(e); } }, - validators: { onChange: accountOnboardingSchema }, - }); - - /* - * Steps - */ - - // Is 0-indexed - const [activeStep, setActiveStep] = useState({ - current: 0, - previous: undefined as number | undefined, + validators: { onSubmit: accountOnboardingSchema }, }); - // State that indicates the page is still loading, so temporarily hide everything - const [mounting, setMounting] = useState(true); - - // Because form steps are positioned absolutely on top of one another, they do - // not affect the document flow and thus the height of the parent does not - // adjust to the different height of steps. - // This variable is determined by the height of the active step, and is used - // to set the height of its parent. - const [formHeight, setFormHeight] = useState(0); - - // Ref that refers to the component for the active step - const measureFormStepRef = useCallback((node: HTMLDivElement | null) => { - if (node !== null) { - // Permanently removes the flag that the page is still loading - // Ideally, this would go in another location, but here works fine for now - setMounting(false); - - const height = node.clientHeight; - setFormHeight(height); - - // As user interacts with the form, the height of the component may change naturally. - // Thus, the parent height will need to be adjusted whenever the active step - // component changes size. - const observer = new ResizeObserver((entries) => { - entries.forEach((entry) => { - setFormHeight(entry.contentRect.height); - }); - }); - - observer.observe(node); - - return () => { - observer.disconnect(); - }; - } - }, []); - - const validateFields = () => { - const step = steps[activeStep.current]; - if (typeof step === 'undefined') { - return true; - } - const fields = step?.fields; - if (typeof fields === 'undefined') { - return true; - } - fields.forEach((step) => form.validateField(step, 'change')); - }; - - const currentFieldsValid = () => { - const step = steps[activeStep.current]; - if (typeof step === 'undefined') { - return true; - } - const fields = step?.fields; - if (typeof fields === 'undefined') { - return true; - } - return fields.every((step) => form.state.fieldMeta[step]?.isValid ?? true); - }; - - const handleNext = (event: MouseEvent) => { - // Validates mounted fields and prevents user from navigating if there exist errors - validateFields(); - if (!currentFieldsValid()) return; - - if (activeStep.current < steps.length - (hasFinish ? 2 : 1)) { - // Prevents submit button from activating prematurely when navigating - // from the penultimate step to the last step - event.preventDefault(); - setActiveStep((prev) => ({ - current: prev.current + 1, - previous: prev.current, - })); - } else if (activeStep.current === steps.length - (hasFinish ? 1 : 0)) { - // When user clicks Continue button on success screen - router.push('/'); - } - }; - - const handleBack = () => { - // Validates mounted fields and prevents user from navigating if there exist errors - validateFields(); - if (!currentFieldsValid()) return; - - if (activeStep.current > 0) { - setActiveStep((prev) => ({ - current: prev.current - 1, - previous: prev.current, - })); - } - }; - - const BackButton = ( - - ); - - const NextButton = ( - - ); - - const OnboardingFormElement = ( -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="flex flex-col gap-8 w-full" - noValidate - > - -
- - {steps.map((step, index) => { - if (!step.hidden) { - return ( - - { - // Validates mounted fields and prevents user from navigating if there exist errors - validateFields(); - if (!currentFieldsValid()) return; - - setActiveStep((prev) => ({ - current: index, - previous: prev.current, - })); - }} - disabled={ - index - 1 > activeStep.current || - activeStep.current === steps.length - 1 || - !currentFieldsValid() - } - > - - {step.label} - - - - ); - } - })} - -
-
- -
- {/* Hidden step component used only to correctly size the parent when the page loads */} -
- -
- - {steps.map((step, index) => { - const isActive = activeStep.current === index; - - // Determines the direction of the slide transition - const direction = - activeStep.previous !== undefined - ? activeStep.current > activeStep.previous - ? // on next - activeStep.current === index - ? // entering - 'left' - : // exiting - 'right' - : // on back - activeStep.current === index - ? // entering - 'right' - : // exiting - 'left' - : // on mount - 'left'; - - return ( - + router.push('/')}> + +
+
+ -
- + + Welcome to UTD Clubs! Let's get you set up. + +
+
+ + + + + + + {(field) => ( + -
-
- ); - })} -
-
- {BackButton} - {NextButton} -
-
-
+ )} + + + {(field) => ( + + )} + + + + + + + + + + {(field) => ( + + )} + + + {(field) => ( + + )} + + + + + {(field) => ( + + )} + + + {(field) => { + return ( + { + field.handleChange(value); + }} + value={field.state.value ?? null} + label="Graduation Date" + className="[&>.MuiPickersInputBase-root]:bg-white dark:[&>.MuiPickersInputBase-root]:bg-neutral-800 w-64 grow" + slotProps={{ + actionBar: { + actions: ['accept'], + }, + textField: { + size: 'small', + error: !field.state.meta.isValid, + helperText: !field.state.meta.isValid + ? ( + field.state.meta.errors as unknown as { + message: string; + }[] + ) + .map((err) => err?.message) + .join('. ') + '.' + : undefined, + required: true, + }, + }} + timezone="UTC" + views={['year', 'month']} + openTo="year" + /> + ); + }} + + + + + + + + + + {(field) => ( +
+ +
+ )} +
+
+
+
+ + +
+
+ + Thank you! + + + You are now ready to use UTD Clubs. You can always change + everything later in your{' '} + + account settings + + . + +
+
+
+ + ); return withLayout ? (
-
{OnboardingFormElement}
+
{FormElement}
) : ( - <>{OnboardingFormElement} + <>{FormElement} + ); +} + +function FormStepContent({ + title, + children, +}: { + title: string; + children: ReactNode; +}) { + return ( +
+ + {title} + +
{children}
+
); } diff --git a/src/components/getting-started/OnboardingFormStep.tsx b/src/components/getting-started/OnboardingFormStep.tsx deleted file mode 100644 index c9151cb6f..000000000 --- a/src/components/getting-started/OnboardingFormStep.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import Typography from '@mui/material/Typography'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import Link from 'next/link'; -import { ReactNode } from 'react'; -import { WizardStepObject } from '@src/components/form/FormWizard'; -import { majors, minors } from '@src/constants/utdDegrees'; -import { studentClassificationEnum } from '@src/server/db/schema/users'; -import { withForm } from '@src/utils/form'; -import { AccountOnboardingSchema } from '@src/utils/formSchemas'; -import { stepIds } from './OnboardingForm'; - -type FormData = Partial; - -const OnboardingFormStep = withForm({ - defaultValues: {} as FormData, - props: { - step: {} as WizardStepObject | undefined, - active: false as boolean | undefined, - }, - render: function Render({ form, step, active }) { - let FormStepData: ReactNode; - - if (step?.id) { - switch (step?.id as stepIds) { - case 1: - FormStepData = ( - - - - {(field) => ( - - )} - - - {(field) => ( - - )} - - - - ); - break; - case 2: - FormStepData = ( - - - - {(field) => ( - - )} - - - {(field) => ( - - )} - - - - - {(field) => ( - - )} - - - {(field) => { - return ( - { - field.handleChange(value); - }} - value={field.state.value ?? null} - label="Graduation Date" - className="w-64 grow" - slotProps={{ - actionBar: { - actions: ['accept'], - }, - textField: { - size: 'small', - error: !field.state.meta.isValid, - helperText: !field.state.meta.isValid - ? ( - field.state.meta.errors as unknown as { - message: string; - }[] - ) - .map((err) => err?.message) - .join('. ') + '.' - : undefined, - required: true, - }, - }} - timezone="UTC" - views={['year', 'month']} - openTo="year" - /> - ); - }} - - - - ); - break; - case 3: - FormStepData = ( - - - - {(field) => ( -
- -
- )} -
-
-
- ); - break; - default: - FormStepData = ( -
- - Whoops! You aren't supposed to see this... - -
- ); - break; - } - } else if (step?.variant === 'start') { - FormStepData = ( -
-
- - Get Started - - - Welcome to UTD Clubs! Let's get you set up. - -
-
- ); - } else if (step?.variant === 'finish') { - FormStepData = ( -
-
- - Thank you! - - - You are now ready to use UTD Clubs. You can always change - everything later in your{' '} - - account settings - - . - -
-
- ); - } - - return ( -
- {FormStepData} -
- ); - }, -}); - -export default OnboardingFormStep; - -function FormStepContent({ - title, - children, -}: { - title: string; - children: ReactNode; -}) { - return ( -
- - {title} - -
{children}
-
- ); -} diff --git a/src/utils/form.ts b/src/utils/form.ts index 3d2611303..bc01519f6 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -8,6 +8,7 @@ import FormFieldSet from '@src/components/form/FormFieldSet'; import FormQuestion from '@src/components/form/FormQuestion'; import FormSelect from '@src/components/form/FormSelect'; import FormTextField from '@src/components/form/FormTextField'; +import { FormWizard, FormWizardStep } from '@src/components/form/FormWizard'; // export useFieldContext for use in your custom components export const { fieldContext, useFieldContext, formContext, useFormContext } = @@ -16,7 +17,6 @@ export const { fieldContext, useFieldContext, formContext, useFormContext } = export const { useAppForm, withForm } = createFormHook({ fieldContext, formContext, - // We'll learn more about these options later fieldComponents: { TextField: FormTextField, Select: FormSelect, @@ -27,5 +27,7 @@ export const { useAppForm, withForm } = createFormHook({ SubmitButton: FormSubmitButton, FieldSet: FormFieldSet, Question: FormQuestion, + Wizard: FormWizard, + WizardStep: FormWizardStep, }, });