Lab forms: multi-step wizard (shared form + coach) and bespoke scroll-to
- FormWizard/WizardStep: a step shell for the long offer forms. Steps stay mounted (hidden, not unmounted) so nested components with their own state (LabExtrasFields) keep their values; a controlled step lets the form jump to the offending step on validation. - Applied to CraftProfileForm (the 8 data-driven crafts, 5 steps: Identity / What you offer / Rates / Contact / Media) and the coach page (5 steps). Submit + Preview live in the wizard footer; submit errors jump to the right step before scrolling. - The 4 bespoke pages now scroll to their inline contact error on a failed submit.
This commit is contained in:
parent
6a0b0695ee
commit
bbe4ba2e14
9 changed files with 192 additions and 53 deletions
|
|
@ -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<string | undefined>(undefined);
|
||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||
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 }) {
|
|||
/>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<FormWizard
|
||||
steps={[
|
||||
{ id: 'identity', title: t('labUi.wizard.identity') },
|
||||
{ id: 'offer', title: t('labUi.wizard.offer') },
|
||||
{ id: 'rates', title: t('labUi.wizard.rates') },
|
||||
{ id: 'contact', title: t('labUi.wizard.contact') },
|
||||
{ id: 'media', title: t('labUi.wizard.media') },
|
||||
]}
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
accentChip={accentChip}
|
||||
submitting={isPending}
|
||||
submitLabel={existingProfile ? t('labUi.craftForm.updateProfile') : t('labUi.craftForm.publishProfile')}
|
||||
submittingLabel={t('labUi.become.publishing')}
|
||||
footerExtra={
|
||||
<Button type="button" variant="outline" onClick={() => setPreviewOpen(true)} className="h-12 flex-shrink-0 gap-2 rounded-xl border-white/15 font-bold">
|
||||
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<WizardStep index={0}>
|
||||
{/* Image + Bio */}
|
||||
<Card className="bg-black/60 border-white/10">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
|
|
@ -484,6 +509,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={1}>
|
||||
{/* Skill sections */}
|
||||
{skillSections.map((section, sectionIndex) => (
|
||||
<Card key={section.title} id={sectionIndex === 0 ? 'craft-skills' : undefined} className="bg-black/60 border-white/10">
|
||||
|
|
@ -553,6 +580,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={2}>
|
||||
{/* Location + experience + rate */}
|
||||
<Card className="bg-black/60 border-white/10">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
|
|
@ -581,6 +610,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={3}>
|
||||
{/* Contact + links */}
|
||||
<Card className="bg-black/60 border-white/10">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
|
|
@ -615,6 +646,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={4}>
|
||||
{/* Shared enrichment: languages, availability, rate, payments, portfolio */}
|
||||
<LabExtrasFields
|
||||
existingTags={existingProfile?.tags}
|
||||
|
|
@ -647,24 +680,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
{/* Pro upgrade (optional) */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText={accentText} entityCoord={entityCoord} />
|
||||
|
||||
{/* Preview + Submit */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
className="h-12 flex-shrink-0 gap-2 rounded-xl border-white/15 font-bold"
|
||||
>
|
||||
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className={cn('flex-1 h-12 rounded-xl font-black text-base uppercase tracking-wide', accentChip)}
|
||||
>
|
||||
{isPending ? <Loader2 className="h-5 w-5 animate-spin" /> : existingProfile ? t('labUi.craftForm.updateProfile') : t('labUi.craftForm.publishProfile')}
|
||||
</Button>
|
||||
</div>
|
||||
</WizardStep>
|
||||
</FormWizard>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
92
src/components/FormWizard.tsx
Normal file
92
src/components/FormWizard.tsx
Normal file
|
|
@ -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 (
|
||||
<WizardCtx.Provider value={step}>
|
||||
<div className="space-y-4">
|
||||
{/* Step nav */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{steps.map((s, i) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
onClick={() => onStepChange(i)}
|
||||
className={cn(
|
||||
'min-h-0 min-w-0 rounded-full border px-3 py-1.5 text-xs font-bold transition-colors',
|
||||
i === step ? accentChip : i < step ? 'border-white/20 text-gray-300' : 'border-white/10 text-gray-500',
|
||||
)}
|
||||
>
|
||||
{i < step && <Check className="mr-1 inline h-3 w-3" />}
|
||||
{i + 1}. {s.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Current step content (others stay mounted but hidden) */}
|
||||
{children}
|
||||
|
||||
{/* Nav */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{footerExtra}
|
||||
{step > 0 && (
|
||||
<Button type="button" variant="outline" onClick={() => onStepChange(step - 1)} className="h-12 gap-1 border-white/15 font-bold">
|
||||
<ChevronLeft className="h-4 w-4" /> {t('common.back')}
|
||||
</Button>
|
||||
)}
|
||||
{last ? (
|
||||
<Button type="submit" disabled={submitting} className={cn('h-12 flex-1 rounded-xl font-black uppercase tracking-wide', accentChip)}>
|
||||
{submitting ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{submittingLabel}</> : submitLabel}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" onClick={() => onStepChange(step + 1)} className={cn('h-12 flex-1 rounded-xl font-black uppercase tracking-wide', accentChip)}>
|
||||
{t('common.next')} <ChevronRight className="ml-1 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</WizardCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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 <div className={current === index ? 'space-y-4' : 'hidden'}>{children}</div>;
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Input id="social" placeholder="@username or link" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
</div>
|
||||
{contactError && <p className="text-sm text-red-400">{contactError}</p>}
|
||||
{contactError && <p id="contact-error" className="text-sm text-red-400">{contactError}</p>}
|
||||
<PaymentReadinessNote />
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
<div className="p-3 rounded-lg border border-amber-500/30 bg-amber-500/5">
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<CardDescription className="text-gray-400">{t('labUi.coachForm.profileDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<FormWizard
|
||||
steps={[
|
||||
{ id: 'identity', title: t('labUi.wizard.identity') },
|
||||
{ id: 'coach', title: t('labUi.coachForm.sectionWhatYouCoach') },
|
||||
{ id: 'creds', title: t('labUi.coachForm.sectionCredentials') },
|
||||
{ id: 'pay', title: t('labUi.coachForm.sectionPayments') },
|
||||
{ id: 'where', title: t('labUi.coachForm.sectionWhereYouCoach') },
|
||||
]}
|
||||
step={step}
|
||||
onStepChange={setStep}
|
||||
accentChip="bg-red-500 text-white border-red-500"
|
||||
submitting={isPublishing || isUploading}
|
||||
submitLabel={existingProfile ? t('labUi.coachForm.update') : isPro ? t('labUi.coachForm.createPro') : t('labUi.coachForm.create')}
|
||||
submittingLabel={t('labUi.become.publishing')}
|
||||
footerExtra={
|
||||
<Button type="button" variant="outline" onClick={() => setPreviewOpen(true)} className="h-12 flex-shrink-0 gap-2 border-gray-700 font-bold">
|
||||
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<WizardStep index={0}>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('labUi.coachForm.identity')}</Label>
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg border border-gray-700 bg-gray-800/50">
|
||||
|
|
@ -641,6 +666,8 @@ export default function BecomeCoachPage() {
|
|||
<CrewLinkField value={crewRef} onChange={setCrewRef} accentBg="bg-[#ff0040] hover:bg-[#ff0040]/90" accentText="text-[#ff0040]" />
|
||||
)}
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={1}>
|
||||
<FormSection title={t('labUi.coachForm.sectionWhatYouCoach')} />
|
||||
|
||||
<div className="space-y-3">
|
||||
|
|
@ -695,6 +722,8 @@ export default function BecomeCoachPage() {
|
|||
idPrefix="age"
|
||||
/>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={2}>
|
||||
<FormSection title={t('labUi.coachForm.sectionCredentials')} />
|
||||
|
||||
<OptionChips
|
||||
|
|
@ -812,6 +841,8 @@ export default function BecomeCoachPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={3}>
|
||||
<FormSection title={t('labUi.coachForm.sectionPayments')} />
|
||||
|
||||
<OptionChips
|
||||
|
|
@ -870,6 +901,8 @@ export default function BecomeCoachPage() {
|
|||
<Input id="payment-note" placeholder={t('labUi.coachForm.paymentNotePlaceholder')} value={paymentNote} onChange={(e) => setPaymentNote(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
|
||||
</WizardStep>
|
||||
<WizardStep index={4}>
|
||||
<FormSection title={t('labUi.coachForm.sectionWhereYouCoach')} />
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -998,33 +1031,8 @@ export default function BecomeCoachPage() {
|
|||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" entityCoord={user ? `${SKATE_COACH_KIND}:${user.pubkey}:coach-profile` : undefined} />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
className="h-10 flex-shrink-0 gap-2 border-gray-700 font-bold"
|
||||
>
|
||||
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1 bg-red-500 hover:bg-red-600 uppercase font-black"
|
||||
disabled={isPublishing || isUploading}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
{t('labUi.become.publishing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GraduationCap className="h-4 w-4 mr-2" />
|
||||
{existingProfile ? t('labUi.coachForm.update') : isPro ? t('labUi.coachForm.createPro') : t('labUi.coachForm.create')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</WizardStep>
|
||||
</FormWizard>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Input placeholder={t('labUi.become.website')} value={website} onChange={(e) => setWebsite(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
<Input placeholder={t('labUi.editorForm.socialPlaceholder')} value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
{contactError && <p className="text-sm text-red-400">{contactError}</p>}
|
||||
{contactError && <p id="contact-error" className="text-sm text-red-400">{contactError}</p>}
|
||||
<PaymentReadinessNote />
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Input id="social" placeholder="@username" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
</div>
|
||||
{contactError && <p className="text-sm text-red-400">{contactError}</p>}
|
||||
{contactError && <p id="contact-error" className="text-sm text-red-400">{contactError}</p>}
|
||||
|
||||
<PaymentReadinessNote />
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Input id="social" placeholder="@username or link" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
</div>
|
||||
{contactError && <p className="text-sm text-red-400">{contactError}</p>}
|
||||
{contactError && <p id="contact-error" className="text-sm text-red-400">{contactError}</p>}
|
||||
<PaymentReadinessNote />
|
||||
<NofferField value={noffer} onChange={setNoffer} />
|
||||
<div className="p-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5">
|
||||
|
|
|
|||
Loading…
Reference in a new issue