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 { useLabDraft } from '@/hooks/useLabDraft';
|
||||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||||
import { LabProfileProgress } from '@/components/LabProfileProgress';
|
import { LabProfileProgress } from '@/components/LabProfileProgress';
|
||||||
|
import { FormWizard, WizardStep } from '@/components/FormWizard';
|
||||||
import { isValidNoffer, type EntityType } from '@/lib/types';
|
import { isValidNoffer, type EntityType } from '@/lib/types';
|
||||||
import type { LabOption } from '@/lib/labExtras';
|
import type { LabOption } from '@/lib/labExtras';
|
||||||
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
|
|
@ -139,6 +140,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
||||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
const draft = useLabDraft(
|
const draft = useLabDraft(
|
||||||
`craft:${kind}:${identifier}`,
|
`craft:${kind}:${identifier}`,
|
||||||
|
|
@ -255,7 +257,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
setAttempted(true);
|
setAttempted(true);
|
||||||
|
|
||||||
if (contactError) {
|
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' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +267,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
for (const section of requiredSections) {
|
for (const section of requiredSections) {
|
||||||
const keys = section.entries.map(([k]) => k);
|
const keys = section.entries.map(([k]) => k);
|
||||||
if (!selected.some((s) => keys.includes(s))) {
|
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' });
|
toast({ title: t('labUi.craftForm.sectionRequired', { section: section.title }), description: t('labUi.craftForm.sectionRequiredDesc', { section: section.title }), variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -426,6 +430,27 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<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 */}
|
{/* Image + Bio */}
|
||||||
<Card className="bg-black/60 border-white/10">
|
<Card className="bg-black/60 border-white/10">
|
||||||
<CardContent className="p-5 space-y-4">
|
<CardContent className="p-5 space-y-4">
|
||||||
|
|
@ -484,6 +509,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={1}>
|
||||||
{/* Skill sections */}
|
{/* Skill sections */}
|
||||||
{skillSections.map((section, sectionIndex) => (
|
{skillSections.map((section, sectionIndex) => (
|
||||||
<Card key={section.title} id={sectionIndex === 0 ? 'craft-skills' : undefined} className="bg-black/60 border-white/10">
|
<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>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={2}>
|
||||||
{/* Location + experience + rate */}
|
{/* Location + experience + rate */}
|
||||||
<Card className="bg-black/60 border-white/10">
|
<Card className="bg-black/60 border-white/10">
|
||||||
<CardContent className="p-5 space-y-4">
|
<CardContent className="p-5 space-y-4">
|
||||||
|
|
@ -581,6 +610,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={3}>
|
||||||
{/* Contact + links */}
|
{/* Contact + links */}
|
||||||
<Card className="bg-black/60 border-white/10">
|
<Card className="bg-black/60 border-white/10">
|
||||||
<CardContent className="p-5 space-y-4">
|
<CardContent className="p-5 space-y-4">
|
||||||
|
|
@ -615,6 +646,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={4}>
|
||||||
{/* Shared enrichment: languages, availability, rate, payments, portfolio */}
|
{/* Shared enrichment: languages, availability, rate, payments, portfolio */}
|
||||||
<LabExtrasFields
|
<LabExtrasFields
|
||||||
existingTags={existingProfile?.tags}
|
existingTags={existingProfile?.tags}
|
||||||
|
|
@ -647,24 +680,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
{/* Pro upgrade (optional) */}
|
{/* Pro upgrade (optional) */}
|
||||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText={accentText} entityCoord={entityCoord} />
|
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText={accentText} entityCoord={entityCoord} />
|
||||||
|
|
||||||
{/* Preview + Submit */}
|
</WizardStep>
|
||||||
<div className="flex gap-2">
|
</FormWizard>
|
||||||
<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>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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',
|
location: 'Location',
|
||||||
contact: 'Contact',
|
contact: 'Contact',
|
||||||
},
|
},
|
||||||
|
wizard: {
|
||||||
|
identity: 'Identity',
|
||||||
|
offer: 'What you offer',
|
||||||
|
rates: 'Rates',
|
||||||
|
contact: 'Contact',
|
||||||
|
media: 'Media',
|
||||||
|
},
|
||||||
offerTattoo: 'Offer Tattoo',
|
offerTattoo: 'Offer Tattoo',
|
||||||
offerTraining: 'Offer Training / Physio',
|
offerTraining: 'Offer Training / Physio',
|
||||||
organizeEvents: 'Organize Events',
|
organizeEvents: 'Organize Events',
|
||||||
|
|
|
||||||
|
|
@ -2021,6 +2021,13 @@ labUi: {
|
||||||
location: 'Ubicación',
|
location: 'Ubicación',
|
||||||
contact: 'Contacto',
|
contact: 'Contacto',
|
||||||
},
|
},
|
||||||
|
wizard: {
|
||||||
|
identity: 'Identidad',
|
||||||
|
offer: 'Qué ofreces',
|
||||||
|
rates: 'Tarifas',
|
||||||
|
contact: 'Contacto',
|
||||||
|
media: 'Multimedia',
|
||||||
|
},
|
||||||
offerTattoo: 'Ofrecer tatuajes',
|
offerTattoo: 'Ofrecer tatuajes',
|
||||||
offerTraining: 'Ofrecer entrenamiento / fisio',
|
offerTraining: 'Ofrecer entrenamiento / fisio',
|
||||||
organizeEvents: 'Organizar eventos',
|
organizeEvents: 'Organizar eventos',
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,13 @@ export default function BecomeBuilderPage() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (contactError) {
|
if (contactError) {
|
||||||
|
document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServices.length === 0) {
|
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' });
|
toast({ title: t('labUi.builderForm.buildTypesRequired'), description: t('labUi.builderForm.selectBuild'), variant: 'destructive' });
|
||||||
return;
|
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" />
|
<Input id="social" placeholder="@username or link" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<PaymentReadinessNote />
|
||||||
<NofferField value={noffer} onChange={setNoffer} />
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
<div className="p-3 rounded-lg border border-amber-500/30 bg-amber-500/5">
|
<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 { LabProfileProgress } from '@/components/LabProfileProgress';
|
||||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||||
import { labProfilePath } from '@/lib/labRoutes';
|
import { labProfilePath } from '@/lib/labRoutes';
|
||||||
|
import { FormWizard, WizardStep } from '@/components/FormWizard';
|
||||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||||
|
|
||||||
|
|
@ -160,6 +161,7 @@ export default function BecomeCoachPage() {
|
||||||
const [titleInput, setTitleInput] = useState('');
|
const [titleInput, setTitleInput] = useState('');
|
||||||
const [prefilled, setPrefilled] = useState(false);
|
const [prefilled, setPrefilled] = useState(false);
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
const draft = useLabDraft(
|
const draft = useLabDraft(
|
||||||
`craft:coach`,
|
`craft:coach`,
|
||||||
|
|
@ -370,13 +372,15 @@ export default function BecomeCoachPage() {
|
||||||
setAttempted(true);
|
setAttempted(true);
|
||||||
|
|
||||||
if (contactError) {
|
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' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSkills.length === 0) {
|
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' });
|
toast({ title: t('labUi.coachForm.skillsRequired'), description: t('labUi.coachForm.selectSkill'), variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -590,6 +594,27 @@ export default function BecomeCoachPage() {
|
||||||
<CardDescription className="text-gray-400">{t('labUi.coachForm.profileDesc')}</CardDescription>
|
<CardDescription className="text-gray-400">{t('labUi.coachForm.profileDesc')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<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">
|
<div className="space-y-2">
|
||||||
<Label>{t('labUi.coachForm.identity')}</Label>
|
<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">
|
<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]" />
|
<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')} />
|
<FormSection title={t('labUi.coachForm.sectionWhatYouCoach')} />
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -695,6 +722,8 @@ export default function BecomeCoachPage() {
|
||||||
idPrefix="age"
|
idPrefix="age"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={2}>
|
||||||
<FormSection title={t('labUi.coachForm.sectionCredentials')} />
|
<FormSection title={t('labUi.coachForm.sectionCredentials')} />
|
||||||
|
|
||||||
<OptionChips
|
<OptionChips
|
||||||
|
|
@ -812,6 +841,8 @@ export default function BecomeCoachPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={3}>
|
||||||
<FormSection title={t('labUi.coachForm.sectionPayments')} />
|
<FormSection title={t('labUi.coachForm.sectionPayments')} />
|
||||||
|
|
||||||
<OptionChips
|
<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" />
|
<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>
|
</div>
|
||||||
|
|
||||||
|
</WizardStep>
|
||||||
|
<WizardStep index={4}>
|
||||||
<FormSection title={t('labUi.coachForm.sectionWhereYouCoach')} />
|
<FormSection title={t('labUi.coachForm.sectionWhereYouCoach')} />
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -998,33 +1031,8 @@ export default function BecomeCoachPage() {
|
||||||
{/* Optional Pro upgrade — free to list without it */}
|
{/* 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} />
|
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" entityCoord={user ? `${SKATE_COACH_KIND}:${user.pubkey}:coach-profile` : undefined} />
|
||||||
|
|
||||||
<div className="flex gap-2">
|
</WizardStep>
|
||||||
<Button
|
</FormWizard>
|
||||||
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>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -227,11 +227,13 @@ export default function BecomeEditorPage() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (contactError) {
|
if (contactError) {
|
||||||
|
document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServices.length === 0) {
|
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' });
|
toast({ title: t('labUi.editorForm.servicesRequired'), description: t('labUi.editorForm.selectService'), variant: 'destructive' });
|
||||||
return;
|
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.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" />
|
<Input placeholder={t('labUi.editorForm.socialPlaceholder')} 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 />
|
<PaymentReadinessNote />
|
||||||
<NofferField value={noffer} onChange={setNoffer} />
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -240,11 +240,13 @@ export default function BecomeFilmerPage() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (contactError) {
|
if (contactError) {
|
||||||
|
document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServices.length === 0) {
|
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' });
|
toast({ title: t('labUi.filmerForm.servicesRequired'), description: t('labUi.filmerForm.selectService'), variant: 'destructive' });
|
||||||
return;
|
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" />
|
<Input id="social" placeholder="@username" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<PaymentReadinessNote />
|
||||||
<NofferField value={noffer} onChange={setNoffer} />
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
|
|
|
||||||
|
|
@ -302,11 +302,13 @@ export default function BecomePhotographerPage() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (contactError) {
|
if (contactError) {
|
||||||
|
document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServices.length === 0) {
|
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' });
|
toast({ title: t('labUi.photoForm.servicesRequired'), description: t('labUi.photoForm.selectService'), variant: 'destructive' });
|
||||||
return;
|
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" />
|
<Input id="social" placeholder="@username or link" value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<PaymentReadinessNote />
|
||||||
<NofferField value={noffer} onChange={setNoffer} />
|
<NofferField value={noffer} onChange={setNoffer} />
|
||||||
<div className="p-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5">
|
<div className="p-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue