Last active
July 30, 2024 23:50
-
-
Save tlawrie/a93c256cf804b1aa34b422eaea2197f0 to your computer and use it in GitHub Desktop.
Multi-step form using RVF (remix validated form) and shadcn Stepper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Select, | |
SelectTrigger, | |
SelectValue, | |
SelectContent, | |
SelectItem, | |
} from '~/components/ui/select' | |
import { Button, buttonVariants } from '~/components/ui/button' | |
import { | |
Card, | |
CardHeader, | |
CardTitle, | |
CardContent, | |
CardFooter, | |
} from '~/components/ui/card' | |
import { ActionFunctionArgs, LoaderFunctionArgs, json, redirect } from '@remix-run/node' | |
import { Label } from '~/components/ui/label' | |
import { Input } from '~/components/ui/input' | |
import { | |
Table, | |
TableBody, | |
TableCell, | |
TableHead, | |
TableHeader, | |
TableRow, | |
} from '~/components/ui/table' | |
import { Step, Stepper, useStepper } from '~/components/stepper' | |
import { withZod } from '@rvf/zod' | |
import { z } from 'zod' | |
import { | |
FieldArray, | |
isValidationErrorResponse, | |
useControlField, | |
useFieldArray, | |
useForm, | |
validationError, | |
} from '@rvf/remix' | |
import { zfd } from 'zod-form-data' | |
import { | |
Link, | |
useActionData, | |
useLoaderData, | |
useSearchParams, | |
useSubmit, | |
} from '@remix-run/react' | |
import { Textarea } from '~/components/ui/textarea' | |
import { TrashIcon } from 'lucide-react' | |
import { createDeclaration, getDeclaration } from '~/clients/declarations.server' | |
import { useState } from 'react' | |
import { ProductType, StatusType } from '~/types/declarations' | |
import { cn } from '~/utils/theme' | |
import { appLink } from '~/config/app' | |
import { Separator } from '~/components/ui/separator' | |
export const loader = async ({ request }: LoaderFunctionArgs) => { | |
const url = new URL(request.url) | |
const searchParams = url.searchParams | |
let declaration = {} | |
if (searchParams.has('ref')) { | |
declaration = await getDeclaration(request, { id: searchParams.get('ref') }) | |
} else if (searchParams.has('month') && searchParams.has('year')) { | |
declaration = await getDeclaration(request, { id: searchParams.get('ref') }) | |
if (!declaration) { | |
declaration = { | |
declaration: { month: searchParams.get('month'), year: searchParams.get('year') }, | |
} | |
} | |
} | |
//TODO: figure out if we should error out the creation if no params are passed | |
console.log('Loading Declaration: ', declaration) | |
return json({ status: 'ok', data: declaration }) | |
} | |
const FormSchema = z.object({}) | |
const validator = withZod(FormSchema) | |
//Preset declaration object | |
const blankDeclaration = { | |
status: StatusType.Draft, | |
product: ProductType.AluminiumSlab, | |
declaration: {}, | |
forecast: {}, | |
} | |
//TODO Implement a Union Validation function and determine a better way to get the form step | |
export const action = async ({ request, params }: ActionFunctionArgs) => { | |
const url = new URL(request.url) | |
const searchParams = url.searchParams | |
const accountSlug = params.account | |
const formData = await request.formData() | |
const formValidatedData = await validator.validate(formData) | |
const formStep = formValidatedData.formId | |
//This means that the service treats it as an update | |
// let id = '' | |
if (searchParams.has('ref')) { | |
blankDeclaration.id = searchParams.get('ref') | |
} | |
if (formStep === 'step-1') { | |
//Create object with status saved | |
const data = await validatorStep1.validate(formData) | |
if (data.error) return validationError(data.error) | |
const mergedDeclaration = { ...blankDeclaration, declaration: data.data } | |
const declaration = await createDeclaration(request, mergedDeclaration) | |
console.log('Step 1 data: ', declaration) | |
return json({ status: 'ok', message: 'Saved step 1', data: declaration }) | |
} else if (formStep === 'step-2') { | |
//Update object with status saved | |
const data = await validatorStep2.validate(formData) | |
if (data.error) return validationError(data.error) | |
return json({ status: 'ok', message: 'Saved step 2', data: '' }) | |
} else if (formStep === 'step-3') { | |
//Update object with status submitted | |
return json({ message: 'Submitted Form' }) | |
} | |
return { message: `Unable to save form` } | |
} | |
const steps = [ | |
{ label: 'Declaration', description: '' }, | |
{ label: 'Forecast', description: '' }, | |
{ label: 'Review & Submit', description: '' }, | |
] | |
const FormSchemaStep1 = z.object({ | |
month: z.string().min(1, 'Declaration month is required.'), | |
year: z.string().min(1, 'Declaration year is required.'), | |
// total: zfd.numeric(z.number().min(0, 'Total is required.')), | |
locations: z.array( | |
z.object({ | |
location: z.string(), | |
measurement: z.string(), | |
quantity: zfd.numeric(z.number().min(0, 'Required.')), | |
}), | |
), | |
comments: z.string().optional(), | |
}) | |
const validatorStep1 = withZod(FormSchemaStep1) | |
const FormSchemaStep2 = z.object({ | |
month: z.string().min(1, 'Declaration month is required.'), | |
year: z.string().min(1, 'Declaration year is required.'), | |
// total: zfd.numeric(z.number().min(0, 'Total is required.')), | |
locations: z.array( | |
z.object({ | |
location: z.string(), | |
measurement: z.string(), | |
quantity: zfd.numeric(z.number().min(0, 'Required.')), | |
}), | |
), | |
comments: z.string().optional(), | |
}) | |
const validatorStep2 = withZod(FormSchemaStep2) | |
export default function DeclarationFormRoute() { | |
const [searchParams, setSearchParams] = useSearchParams() | |
// const { setStep } = useStepper() | |
let initialStep = 0 | |
if (searchParams.has('s')) { | |
initialStep = parseInt(searchParams.get('s') || '0') | |
// setStep(parseInt(searchParams.get('s') || '0')) | |
} | |
return ( | |
<div className="space-y-6 pt-6"> | |
<div className="flex items-center justify-between"> | |
<h1 className="text-3xl font-bold">Declaration request</h1> | |
</div> | |
<Stepper variant="circle-alt" initialStep={initialStep} steps={steps}> | |
{steps.map((stepProps, index) => { | |
if (index === 0) { | |
return ( | |
<Step key={stepProps.label} {...stepProps}> | |
<FormStep1 /> | |
</Step> | |
) | |
} | |
if (index === 1) { | |
return ( | |
<Step key={stepProps.label} {...stepProps}> | |
<FormStep2 /> | |
</Step> | |
) | |
} | |
return ( | |
<Step key={stepProps.label} {...stepProps}> | |
<FormStep3 /> | |
</Step> | |
) | |
})} | |
<MyStepperFooter /> | |
</Stepper> | |
</div> | |
) | |
} | |
function FormStep1() { | |
const { nextStep } = useStepper() | |
const submit = useSubmit() | |
const { status, data } = useLoaderData<typeof loader>() | |
const result = useActionData<typeof action>() | |
// const [total, setTotal] = useState<Record<number, number>>({}) | |
/* These can come from the loader */ | |
const defaultValues = { | |
month: data.declaration.month || 'january', | |
year: data.declaration.year || '2024', | |
total: 0, | |
locations: data.declaration.locations || [ | |
{ location: '', measurement: '', quantity: 0 }, | |
], | |
comments: '', | |
} | |
//Submission will be handled via the Remix Action | |
const form = useForm({ | |
defaultValues, | |
validator: validatorStep1, | |
method: 'post', | |
id: 'step-1', | |
onSubmitSuccess: () => { | |
if (isValidationErrorResponse(result)) return | |
nextStep() | |
}, | |
}) | |
return ( | |
<form {...form.getFormProps()}> | |
<Card> | |
<CardHeader> | |
<CardTitle>Declaration</CardTitle> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
<div className="space-y-2"> | |
<Label htmlFor="product">Product</Label> | |
<Input id="product" value="Aluminium Slab" disabled /> | |
{/* <Select {...form.getInputProps('product')} disabled> | |
<SelectTrigger id="product"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="Aluminium_slab">Alluminum Slab</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error('product') && <span>{form.error('product')}</span>} */} | |
</div> | |
<div className="flex flex-row space-x-2"> | |
<div className="min-w-80 space-y-2"> | |
<Label htmlFor="month">Month</Label> | |
<Select {...form.getInputProps('month')}> | |
<SelectTrigger | |
id="month" | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="january">January</SelectItem> | |
<SelectItem value="february">February</SelectItem> | |
<SelectItem value="march">March</SelectItem> | |
<SelectItem value="april">April</SelectItem> | |
<SelectItem value="may">May</SelectItem> | |
<SelectItem value="june">June</SelectItem> | |
<SelectItem value="july">July</SelectItem> | |
<SelectItem value="august">August</SelectItem> | |
<SelectItem value="september">September</SelectItem> | |
<SelectItem value="october">October</SelectItem> | |
<SelectItem value="november">November</SelectItem> | |
<SelectItem value="december">December</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error('month') && <span>{form.error('month')}</span>} | |
</div> | |
<div className="min-w-80 space-y-2"> | |
<Label htmlFor="year">Year</Label> | |
<Select {...form.getInputProps('year')}> | |
<SelectTrigger | |
id="year" | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="2024">2024</SelectItem> | |
<SelectItem value="2025">2025</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error('year') && <span>{form.error('year')}</span>} | |
</div> | |
</div> | |
<Table> | |
<TableHeader> | |
<TableRow className="border-none"> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Location</Label> | |
</TableHead> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Unit of measurement</Label> | |
</TableHead> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Quantity</Label> | |
</TableHead> | |
</TableRow> | |
</TableHeader> | |
<TableBody> | |
{form.array('locations').map((key, item, index) => ( | |
<TableRow key={key} className="border-none"> | |
<TableCell className="pl-0"> | |
<Label htmlFor={item.name('location')} className="sr-only"> | |
Location | |
</Label> | |
<Select {...item.getInputProps('location')}> | |
<SelectTrigger | |
id={item.name('location')} | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="logan">Logan</SelectItem> | |
<SelectItem value="oswego">Oswego</SelectItem> | |
</SelectContent> | |
</Select> | |
{item.error('location') && <span>{item.error('location')}</span>} | |
</TableCell> | |
<TableCell className="pl-0"> | |
<Label htmlFor={item.name('measurement')} className="sr-only"> | |
Unit of measurement | |
</Label> | |
<Select {...item.getInputProps('measurement')}> | |
<SelectTrigger | |
id={item.name('measurement')} | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="kg">Kilogram</SelectItem> | |
<SelectItem value="ton">Ton</SelectItem> | |
<SelectItem value="lb">Pound</SelectItem> | |
</SelectContent> | |
</Select> | |
{item.error('measurement') && ( | |
<span>{item.error('measurement')}</span> | |
)} | |
</TableCell> | |
<TableCell className="pl-0"> | |
<Label htmlFor={item.name('quantity')} className="sr-only"> | |
Quantity | |
</Label> | |
<Input | |
type="number" | |
min="0" | |
{...item.getInputProps('quantity')} | |
// onChange={(e) => handleQuantityChange(index, e)} | |
className="rounded-md border border-gray-300 shadow-sm" | |
/> | |
{item.error('quantity') && <span>{item.error('quantity')}</span>} | |
</TableCell> | |
<TableCell> | |
{index !== 0 ? ( | |
<Button | |
onClick={() => { | |
form.array('locations').remove(index) | |
}} | |
variant="ghost" | |
size="icon" | |
type="button"> | |
<TrashIcon /> | |
</Button> | |
) : ( | |
<div className="h-9 w-9"></div> | |
)} | |
</TableCell> | |
</TableRow> | |
))} | |
</TableBody> | |
</Table> | |
<Button | |
variant="outline" | |
className={cn( | |
buttonVariants({ variant: 'outline' }), | |
'text-[#580720]) mt-4 border-[#580720] bg-[#FDFCFC]', | |
)} | |
type="button" | |
onClick={() => | |
form | |
.array('locations') | |
.insert(1, { location: '', measurement: '', quantity: 0 }) | |
}> | |
+ Add location | |
</Button> | |
{form.array('locations').error() && ( | |
<div>{form.array('locations').error()}</div> | |
)} | |
<div className="mt-4 space-y-2"> | |
<Label htmlFor="comments">Comments</Label> | |
<Textarea | |
{...form.getInputProps('comments')} | |
placeholder="A lovely placeholder" | |
className="rounded-md border border-gray-300 shadow-sm" | |
/> | |
</div> | |
</div> | |
</CardContent> | |
<CardFooter className="flex justify-between"> | |
<StepperFormActions | |
isSubmitting={form.formState.isSubmitting} | |
showBack={false} | |
/> | |
</CardFooter> | |
</Card> | |
{/* Useful debugging - work into the wrapper */} | |
<p>{form.formState.isValid ? 'Valid!' : 'Not Valid'}</p> | |
{result && !isValidationErrorResponse(result) && <p>{result.message}</p>} | |
{Object.entries(form.formState.fieldErrors).map(([key, error]) => ( | |
<p> | |
{key}:{error} | |
</p> | |
))} | |
{/* </ValidatedForm> */} | |
</form> | |
) | |
} | |
//TODO, pass in the data between steps. probably by hoisting the onSubmitSuccess or the action into the higher wrapping function and passing down to both steps. | |
//TODO: this shares a majority of the same fields as Step 1 | |
// find a way to componetise and pass in a different scope | |
function FormStep2() { | |
const { nextStep } = useStepper() | |
const result = useActionData<typeof action>() | |
const form = useForm({ | |
defaultValues: { | |
month: '', | |
year: '', | |
total: 0, | |
locations: [{ location: '', measurement: '', quantity: 0 }], | |
comments: '', | |
}, | |
validator: validatorStep2, | |
id: 'step-2', | |
method: 'post', | |
// handleSubmit: async (_data: z.infer<typeof FormStep1Schema>) => { | |
// console.log(_data) | |
// nextStep() | |
// }, | |
onSubmitSuccess: () => { | |
if (isValidationErrorResponse(result)) return | |
nextStep() | |
}, | |
}) | |
return ( | |
<form {...form.getFormProps()}> | |
<Card> | |
<CardHeader> | |
<CardTitle>Forecast</CardTitle> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
<div className="flex flex-row space-x-2"> | |
<div className="min-w-80 space-y-2"> | |
<Label htmlFor="month">Month</Label> | |
<Select {...form.getInputProps('month')}> | |
<SelectTrigger | |
id="month" | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="january">January</SelectItem> | |
<SelectItem value="february">February</SelectItem> | |
<SelectItem value="march">March</SelectItem> | |
<SelectItem value="april">April</SelectItem> | |
<SelectItem value="may">May</SelectItem> | |
<SelectItem value="june">June</SelectItem> | |
<SelectItem value="july">July</SelectItem> | |
<SelectItem value="august">August</SelectItem> | |
<SelectItem value="september">September</SelectItem> | |
<SelectItem value="october">October</SelectItem> | |
<SelectItem value="november">November</SelectItem> | |
<SelectItem value="december">December</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error('month') && <span>{form.error('month')}</span>} | |
</div> | |
<div className="min-w-80 space-y-2"> | |
<Label htmlFor="year">Year</Label> | |
<Select {...form.getInputProps('year')}> | |
<SelectTrigger | |
id="year" | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="2024">2024</SelectItem> | |
<SelectItem value="2025">2025</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error('year') && <span>{form.error('year')}</span>} | |
</div> | |
</div> | |
<Table> | |
<TableHeader> | |
<TableRow className="border-none"> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Location</Label> | |
</TableHead> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Unit of measurement</Label> | |
</TableHead> | |
<TableHead className="pl-0"> | |
<Label className="text-foreground">Quantity</Label> | |
</TableHead> | |
</TableRow> | |
</TableHeader> | |
<TableBody> | |
{/* {items.map(({ defaultValue, key }, index) => ( */} | |
{/* {(array) => ( | |
<> */} | |
{form.array('locations').map((key, item, index) => ( | |
<TableRow key={key} className="border-none"> | |
<TableCell className="pl-0"> | |
<Label htmlFor={`locations[${index}].location`} className="sr-only"> | |
Location | |
</Label> | |
<Select {...form.getInputProps(`locations[${index}].location`)}> | |
<SelectTrigger | |
id={`locations[${index}].location`} | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="logan">Logan</SelectItem> | |
<SelectItem value="oswego">Oswego</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error(`locations[${index}].location`) && ( | |
<span>{form.error(`locations[${index}].location`)}</span> | |
)} | |
</TableCell> | |
<TableCell className="pl-0"> | |
<Label | |
htmlFor={`locations[${index}].measurement`} | |
className="sr-only"> | |
Unit of measurement | |
</Label> | |
<Select {...form.getInputProps(`locations[${index}].measurement`)}> | |
<SelectTrigger | |
id={`locations[${index}].measurement`} | |
className="rounded-md border border-gray-300 shadow-sm"> | |
<SelectValue placeholder="Select" /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="kg">Kilogram</SelectItem> | |
<SelectItem value="ton">Ton</SelectItem> | |
<SelectItem value="lb">Pound</SelectItem> | |
</SelectContent> | |
</Select> | |
{form.error(`locations[${index}].measurement`) && ( | |
<span>{form.error(`locations[${index}].measurement`)}</span> | |
)} | |
</TableCell> | |
<TableCell className="pl-0"> | |
<Label htmlFor={`locations[${index}].quantity`} className="sr-only"> | |
Quantity | |
</Label> | |
<Input | |
type="number" | |
min="0" | |
{...form.getInputProps(`locations[${index}].quantity`)} | |
// onChange={(e) => handleQuantityChange(index, e)} | |
className="rounded-md border border-gray-300 shadow-sm" | |
/> | |
{form.error(`locations[${index}].quantity`) && ( | |
<span>{form.error(`locations[${index}].quantity`)}</span> | |
)} | |
</TableCell> | |
<TableCell> | |
{index !== 0 ? ( | |
<Button | |
onClick={() => { | |
form.array('locations').remove(index) | |
}} | |
variant="ghost" | |
size="icon" | |
type="button"> | |
<TrashIcon /> | |
</Button> | |
) : ( | |
<div className="h-9 w-9"></div> | |
)} | |
</TableCell> | |
</TableRow> | |
))} | |
{/* </> */} | |
{/* )} */} | |
</TableBody> | |
</Table> | |
<Button | |
variant="outline" | |
className={cn( | |
buttonVariants({ variant: 'outline' }), | |
'text-[#580720]) mt-4 border-[#580720] bg-[#FDFCFC]', | |
)} | |
type="button" | |
onClick={() => | |
form | |
.array('locations') | |
.insert(1, { location: '', measurement: '', quantity: 0 }) | |
}> | |
+ Add location | |
</Button> | |
{/* <div className="mt-4 space-y-2"> | |
<Label htmlFor="total">Total</Label> | |
<Input {...form.getInputProps('total')} type="number" min="0" readOnly /> | |
{form.error('total') && <span>{form.error('total')}</span>} | |
</div> */} | |
<div className="mt-4 space-y-2"> | |
<Label htmlFor="comments">Comments</Label> | |
<Textarea | |
{...form.getInputProps('comments')} | |
placeholder="A lovely placeholder" | |
/> | |
</div> | |
</div> | |
</CardContent> | |
<CardFooter className="flex justify-between"> | |
<StepperFormActions isSubmitting={form.formState.isSubmitting} /> | |
</CardFooter> | |
</Card> | |
{/* Useful debugging - work into the wrapper */} | |
<p>{form.formState.isValid ? 'Valid!' : 'Not Valid'}</p> | |
{result && !isValidationErrorResponse(result) && <p>{result.message}</p>} | |
{Object.entries(form.formState.fieldErrors).map(([key, error]) => ( | |
<p> | |
{key}:{error} | |
</p> | |
))} | |
</form> | |
) | |
} | |
function FormStep3() { | |
const { status, data } = useLoaderData<typeof loader>() | |
return ( | |
<Card> | |
<CardHeader> | |
<CardTitle>Aluminium Slab</CardTitle> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
<div className="flex flex-row space-x-2"> | |
<SummaryItem | |
label="Declaration month" | |
content={`${data.declaration.month} ${data.declaration.year}`} | |
/> | |
</div> | |
<div className="grid grid-cols-3"> | |
<SummaryLabel>Location</SummaryLabel> | |
<SummaryLabel>Unit of measurement</SummaryLabel> | |
<SummaryLabel>Quantity</SummaryLabel> | |
</div> | |
{data && | |
data.declaration.locations.map((item, index) => ( | |
<div className="grid grid-cols-3"> | |
<p>{item.location}</p> | |
<p>{item.measurement}</p> | |
<p>{item.quantity}</p> | |
</div> | |
))} | |
<SummaryItem label="Comments" content={data.declaration.comments} /> | |
<Separator /> | |
<div className="flex flex-row space-x-2"> | |
<SummaryItem | |
label="Forecast month" | |
content={`${data.forecast.month} ${data.forecast.year}`} | |
/> | |
</div> | |
<div className="grid grid-cols-3"> | |
<SummaryLabel>Location</SummaryLabel> | |
<SummaryLabel>Unit of measurement</SummaryLabel> | |
<SummaryLabel>Quantity</SummaryLabel> | |
</div> | |
{data && | |
data.forecast.locations.map((item, index) => ( | |
<div className="grid grid-cols-3"> | |
<p>{item.location}</p> | |
<p>{item.measurement}</p> | |
<p>{item.quantity}</p> | |
</div> | |
))} | |
<SummaryItem label="Comments" content={data.forecast.comments} /> | |
</div> | |
</CardContent> | |
<CardFooter className="flex justify-between"> | |
<StepperFormActions /> | |
</CardFooter> | |
</Card> | |
) | |
} | |
function SummaryLabel({ children }: { children: string }) { | |
return <p className="font-[#4D4D4D] text-xs tracking-wide">{children}</p> | |
} | |
function SummaryItem({ label, content }: { label: string; content: string }) { | |
return ( | |
<div className="min-w-80 space-y-2"> | |
<SummaryLabel className="font-[#4D4D4D] text-xs tracking-wide"> | |
{label} | |
</SummaryLabel> | |
<p className="">{content}</p> | |
</div> | |
) | |
} | |
function StepperFormActions({ | |
isSubmitting = false, | |
showBack = true, | |
account = '', | |
}: { | |
isSubmitting?: boolean | |
showBack?: boolean | |
account?: string | |
}) { | |
const { | |
prevStep, | |
nextStep, | |
resetSteps, | |
isDisabledStep, | |
hasCompletedAllSteps, | |
isLastStep, | |
activeStep, | |
} = useStepper() | |
return ( | |
<div className="flex w-full justify-between gap-2"> | |
<Link | |
className={cn( | |
buttonVariants({ variant: 'outline' }), | |
'text-[#580720]) border-[#580720] bg-[#FDFCFC]', | |
)} | |
to={appLink.declarations({ | |
account: account, | |
})}> | |
Cancel | |
</Link> | |
{/* <Button type="button" variant="secondary" onClick={resetSteps}> | |
Cancel | |
</Button> */} | |
<div className="flex flex-row gap-4"> | |
{showBack && ( | |
<Button | |
// disabled={isDisabledStep} | |
onClick={prevStep} | |
variant="secondary" | |
type="button"> | |
Back | |
</Button> | |
)} | |
<Button type="submit"> | |
{isLastStep ? 'Submit' : isSubmitting ? 'Saving...!' : 'Save & Continue'} | |
</Button> | |
</div> | |
</div> | |
) | |
} | |
function MyStepperFooter() { | |
const { activeStep, resetSteps, steps } = useStepper() | |
if (activeStep !== steps.length) { | |
return null | |
} | |
return ( | |
<div className="flex items-center justify-end gap-2"> | |
<Button onClick={resetSteps}>Reset Stepper with Form</Button> | |
</div> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment