diff --git a/src/components/CraftProfileForm.tsx b/src/components/CraftProfileForm.tsx index 81e812d..9efd946 100644 --- a/src/components/CraftProfileForm.tsx +++ b/src/components/CraftProfileForm.tsx @@ -26,6 +26,7 @@ import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile'; import { useLabDraft } from '@/hooks/useLabDraft'; import { LabDraftBanner } from '@/components/LabDraftBanner'; import { LabProfileProgress } from '@/components/LabProfileProgress'; +import { FormWizard, WizardStep } from '@/components/FormWizard'; import { isValidNoffer, type EntityType } from '@/lib/types'; import type { LabOption } from '@/lib/labExtras'; import { labProfilePathFromKind } from '@/lib/labRoutes'; @@ -139,6 +140,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { const [existingGeohash, setExistingGeohash] = useState(undefined); const [extraTags, setExtraTags] = useState([]); const [previewOpen, setPreviewOpen] = useState(false); + const [step, setStep] = useState(0); const draft = useLabDraft( `craft:${kind}:${identifier}`, @@ -255,7 +257,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { setAttempted(true); if (contactError) { - scrollTo(emailError ? 'craft-email' : websiteError ? 'craft-website' : 'craft-portfolio'); + setStep(3); + window.setTimeout(() => scrollTo(emailError ? 'craft-email' : websiteError ? 'craft-website' : 'craft-portfolio'), 60); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } @@ -264,7 +267,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { for (const section of requiredSections) { const keys = section.entries.map(([k]) => k); if (!selected.some((s) => keys.includes(s))) { - scrollTo('craft-skills'); + setStep(1); + window.setTimeout(() => scrollTo('craft-skills'), 60); toast({ title: t('labUi.craftForm.sectionRequired', { section: section.title }), description: t('labUi.craftForm.sectionRequiredDesc', { section: section.title }), variant: 'destructive' }); return; } @@ -426,6 +430,27 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { />
+ setPreviewOpen(true)} className="h-12 flex-shrink-0 gap-2 rounded-xl border-white/15 font-bold"> + {t('labUi.preview.button')} + + } + > + {/* Image + Bio */} @@ -484,6 +509,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { + + {/* Skill sections */} {skillSections.map((section, sectionIndex) => ( @@ -553,6 +580,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { )} + + {/* Location + experience + rate */} @@ -581,6 +610,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { + + {/* Contact + links */} @@ -615,6 +646,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) { + + {/* Shared enrichment: languages, availability, rate, payments, portfolio */} - {/* Preview + Submit */} -
- - -
+
+
diff --git a/src/components/FormWizard.tsx b/src/components/FormWizard.tsx new file mode 100644 index 0000000..7ed09c2 --- /dev/null +++ b/src/components/FormWizard.tsx @@ -0,0 +1,92 @@ +import { createContext, useContext, type ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { useTranslation } from '@/contexts/language'; +import { cn } from '@/lib/utils'; +import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; + +const WizardCtx = createContext(0); + +export interface WizardStepDef { + id: string; + title: string; +} + +interface FormWizardProps { + steps: WizardStepDef[]; + /** Controlled current step (owned by the form so it can jump on validation). */ + step: number; + onStepChange: (step: number) => void; + accentChip: string; + submitting: boolean; + submitLabel: string; + submittingLabel: string; + /** Extra footer node (e.g. a Preview button) shown next to Back/Next. */ + footerExtra?: ReactNode; + children: ReactNode; +} + +/** + * A lightweight multi-step shell for the long Lab offer forms. Steps are + * declared as {@link WizardStepDef} metadata, and each block of the form is + * wrapped in a {@link WizardStep}. Crucially, non-active steps are hidden with + * `display:none` rather than unmounted, so nested components that own state + * (e.g. `LabExtrasFields`) keep their values while you move around. + */ +export function FormWizard({ + steps, step, onStepChange, accentChip, submitting, submitLabel, submittingLabel, footerExtra, children, +}: FormWizardProps) { + const { t } = useTranslation(); + const last = step === steps.length - 1; + + return ( + +
+ {/* Step nav */} +
+ {steps.map((s, i) => ( + + ))} +
+ + {/* Current step content (others stay mounted but hidden) */} + {children} + + {/* Nav */} +
+ {footerExtra} + {step > 0 && ( + + )} + {last ? ( + + ) : ( + + )} +
+
+
+ ); +} + +/** A block of the form shown only while its step is active (kept mounted). */ +export function WizardStep({ index, children }: { index: number; children: ReactNode }) { + const current = useContext(WizardCtx); + return
{children}
; +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7ae3aec..0da425e 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2021,6 +2021,13 @@ labUi: { location: 'Location', contact: 'Contact', }, + wizard: { + identity: 'Identity', + offer: 'What you offer', + rates: 'Rates', + contact: 'Contact', + media: 'Media', + }, offerTattoo: 'Offer Tattoo', offerTraining: 'Offer Training / Physio', organizeEvents: 'Organize Events', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 62ea146..bb69d30 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -2021,6 +2021,13 @@ labUi: { location: 'Ubicación', contact: 'Contacto', }, + wizard: { + identity: 'Identidad', + offer: 'Qué ofreces', + rates: 'Tarifas', + contact: 'Contacto', + media: 'Multimedia', + }, offerTattoo: 'Ofrecer tatuajes', offerTraining: 'Ofrecer entrenamiento / fisio', organizeEvents: 'Organizar eventos', diff --git a/src/pages/BecomeBuilderPage.tsx b/src/pages/BecomeBuilderPage.tsx index a604adf..2d9c4a6 100644 --- a/src/pages/BecomeBuilderPage.tsx +++ b/src/pages/BecomeBuilderPage.tsx @@ -288,11 +288,13 @@ export default function BecomeBuilderPage() { e.preventDefault(); if (contactError) { + document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedServices.length === 0) { + document.querySelector('form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast({ title: t('labUi.builderForm.buildTypesRequired'), description: t('labUi.builderForm.selectBuild'), variant: 'destructive' }); return; } @@ -633,7 +635,7 @@ export default function BecomeBuilderPage() { setSocial(e.target.value)} className="bg-gray-800 border-gray-700" /> - {contactError &&

{contactError}

} + {contactError &&

{contactError}

}
diff --git a/src/pages/BecomeCoachPage.tsx b/src/pages/BecomeCoachPage.tsx index 4be2e1d..302f18f 100644 --- a/src/pages/BecomeCoachPage.tsx +++ b/src/pages/BecomeCoachPage.tsx @@ -37,6 +37,7 @@ import { LabDraftBanner } from '@/components/LabDraftBanner'; import { LabProfileProgress } from '@/components/LabProfileProgress'; import { useLabDraft } from '@/hooks/useLabDraft'; import { labProfilePath } from '@/lib/labRoutes'; +import { FormWizard, WizardStep } from '@/components/FormWizard'; import { usePlatformFee } from '@/hooks/usePlatformFee'; import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile'; @@ -160,6 +161,7 @@ export default function BecomeCoachPage() { const [titleInput, setTitleInput] = useState(''); const [prefilled, setPrefilled] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); + const [step, setStep] = useState(0); const draft = useLabDraft( `craft:coach`, @@ -370,13 +372,15 @@ export default function BecomeCoachPage() { setAttempted(true); if (contactError) { - scrollTo(emailError ? 'email' : 'website'); + setStep(4); + window.setTimeout(() => scrollTo(emailError ? 'email' : 'website'), 60); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedSkills.length === 0) { - scrollTo('coach-skills'); + setStep(1); + window.setTimeout(() => scrollTo('coach-skills'), 60); toast({ title: t('labUi.coachForm.skillsRequired'), description: t('labUi.coachForm.selectSkill'), variant: 'destructive' }); return; } @@ -590,6 +594,27 @@ export default function BecomeCoachPage() { {t('labUi.coachForm.profileDesc')} + setPreviewOpen(true)} className="h-12 flex-shrink-0 gap-2 border-gray-700 font-bold"> + {t('labUi.preview.button')} + + } + > +
@@ -641,6 +666,8 @@ export default function BecomeCoachPage() { )} + +
@@ -695,6 +722,8 @@ export default function BecomeCoachPage() { idPrefix="age" /> + +
+
+ setPaymentNote(e.target.value)} className="bg-gray-800 border-gray-700" />
+ +
@@ -998,33 +1031,8 @@ export default function BecomeCoachPage() { {/* Optional Pro upgrade — free to list without it */} -
- - -
+ + diff --git a/src/pages/BecomeEditorPage.tsx b/src/pages/BecomeEditorPage.tsx index 3b9ee66..6640b49 100644 --- a/src/pages/BecomeEditorPage.tsx +++ b/src/pages/BecomeEditorPage.tsx @@ -227,11 +227,13 @@ export default function BecomeEditorPage() { e.preventDefault(); if (contactError) { + document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedServices.length === 0) { + document.querySelector('form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast({ title: t('labUi.editorForm.servicesRequired'), description: t('labUi.editorForm.selectService'), variant: 'destructive' }); return; } @@ -561,7 +563,7 @@ export default function BecomeEditorPage() { setWebsite(e.target.value)} className="bg-gray-800 border-gray-700" /> setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
- {contactError &&

{contactError}

} + {contactError &&

{contactError}

}
diff --git a/src/pages/BecomeFilmerPage.tsx b/src/pages/BecomeFilmerPage.tsx index d43fd61..139b4fe 100644 --- a/src/pages/BecomeFilmerPage.tsx +++ b/src/pages/BecomeFilmerPage.tsx @@ -240,11 +240,13 @@ export default function BecomeFilmerPage() { e.preventDefault(); if (contactError) { + document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedServices.length === 0) { + document.querySelector('form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast({ title: t('labUi.filmerForm.servicesRequired'), description: t('labUi.filmerForm.selectService'), variant: 'destructive' }); return; } @@ -593,7 +595,7 @@ export default function BecomeFilmerPage() { setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
- {contactError &&

{contactError}

} + {contactError &&

{contactError}

} diff --git a/src/pages/BecomePhotographerPage.tsx b/src/pages/BecomePhotographerPage.tsx index 53139b7..0b65f80 100644 --- a/src/pages/BecomePhotographerPage.tsx +++ b/src/pages/BecomePhotographerPage.tsx @@ -302,11 +302,13 @@ export default function BecomePhotographerPage() { e.preventDefault(); if (contactError) { + document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedServices.length === 0) { + document.querySelector('form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast({ title: t('labUi.photoForm.servicesRequired'), description: t('labUi.photoForm.selectService'), variant: 'destructive' }); return; } @@ -662,7 +664,7 @@ export default function BecomePhotographerPage() { setSocial(e.target.value)} className="bg-gray-800 border-gray-700" /> - {contactError &&

{contactError}

} + {contactError &&

{contactError}

}