Redesign create-crew into a guided, idiotproof wizard

Audit finding: the crew form was one 1,100-line endless scroll with ~15
sections in a punishing order (logo dead-last, MLS chat coordinator sitting
inline with the crew name), only one validation gate, and no sense of
progress or what's optional.

Fix — split the flow by intent:
- Create mode: a guided 4-step wizard with a sticky progress bar, per-step
  hints, Back/Continue + "Skip to end", and a sticky footer nav.
    1. The Basics   — logo, name, about, home turf (all you actually need)
    2. Identity & Vibe — vibe tags, crew type
    3. Squad & Spots  — members/roles, home spots, open call
    4. Extras         — treasury, video, sponsor, links, band, NIP-05,
                        agent, and the advanced chat coordinator (reframed
                        in plain language, clearly skippable)
  Only the crew name is gated, and only at step 1. Clickable step dots allow
  jumping back freely.
- Edit mode: unchanged UX intent — renders every section flat on one page
  (power users want it all at once).

All field state and the entire publish/handleSubmit logic are preserved
verbatim; sections were extracted into composable blocks with zero behavior
change. Softened the coordinator copy and the map-pin section so a first-timer
isn't scared off.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-03 00:44:59 +07:00
parent a2c34800ce
commit 5716870863
2 changed files with 303 additions and 87 deletions

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, type ReactNode } from 'react';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import ngeohash from 'ngeohash'; import ngeohash from 'ngeohash';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -153,6 +153,12 @@ export function CrewForm({ existing }: CrewFormProps) {
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const { toast } = useToast(); const { toast } = useToast();
// Create flow = guided wizard; edit flow = flat single page (power users
// want everything at once). The wizard staggers the ~15 sections into 4
// digestible steps so a first-timer can spin up a crew in seconds.
const isEditing = !!existing;
const [step, setStep] = useState(0);
const [name, setName] = useState(existing?.name ?? ''); const [name, setName] = useState(existing?.name ?? '');
const [bio, setBio] = useState(existing?.bio ?? ''); const [bio, setBio] = useState(existing?.bio ?? '');
const [location, setLocation] = useState(existing?.location ?? ''); const [location, setLocation] = useState(existing?.location ?? '');
@ -488,26 +494,76 @@ export function CrewForm({ existing }: CrewFormProps) {
} }
}; };
return ( // ─── Section blocks ─────────────────────────────────────────────────────
<form onSubmit={handleSubmit}> // Each visible field group is a self-contained element that closes over the
<Card className="bg-gray-900 border-gray-700"> // component's state. They're composed into wizard steps (create) or one flat
<CardHeader> // scroll (edit) below — no field or publish logic changes between modes.
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5 text-teal-400" /> {existing ? 'Edit Crew' : 'Crew Details'}
</CardTitle>
<CardDescription className="text-gray-400">Your crew's identity, home turf and members</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="crew-name">Crew Name *</Label>
<Input id="crew-name" placeholder="e.g. Backyard Bandits" value={name} onChange={(e) => setName(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2"> const logoBlock = (
<Label htmlFor="crew-bio">About / Manifesto</Label> <div className="space-y-2">
<Textarea id="crew-bio" placeholder="Who you are, what you skate, your story..." value={bio} onChange={(e) => setBio(e.target.value)} rows={5} className="bg-gray-800 border-gray-700" /> <Label>Crew Logo / Photo</Label>
</div> {image ? (
<div className="relative w-32 h-32 rounded-2xl overflow-hidden bg-gray-800">
<img src={image} alt="Crew" className="w-full h-full object-cover" />
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500">
<X className="h-4 w-4" />
</button>
</div>
) : (
<label className="w-32 h-32 rounded-2xl border-2 border-dashed border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-800/50 hover:border-teal-500/50 transition-colors">
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} disabled={isUploading} />
{isUploading ? <Loader2 className="h-6 w-6 animate-spin" /> : <><Upload className="h-6 w-6 mb-1 text-gray-500" /><span className="text-xs text-gray-500">Upload</span></>}
</label>
)}
</div>
);
const nameBlock = (
<div className="space-y-2">
<Label htmlFor="crew-name">Crew Name *</Label>
<Input id="crew-name" placeholder="e.g. Backyard Bandits" value={name} onChange={(e) => setName(e.target.value)} className="bg-gray-800 border-gray-700 text-base" autoFocus={!isEditing} />
</div>
);
const bioBlock = (
<div className="space-y-2">
<Label htmlFor="crew-bio">About / Manifesto</Label>
<Textarea id="crew-bio" placeholder="Who you are, what you skate, your story..." value={bio} onChange={(e) => setBio(e.target.value)} rows={5} className="bg-gray-800 border-gray-700" />
</div>
);
const locationBlock = (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="crew-location">Home Turf (City / Area)</Label>
<Input id="crew-location" placeholder="Bristol, UK" value={location} onChange={(e) => setLocation(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="crew-founded">Founded (Year)</Label>
<Input id="crew-founded" type="number" placeholder="2015" value={founded} onChange={(e) => setFounded(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
</div>
<div className="space-y-2">
<Label>Map Pin <span className="font-normal text-gray-500">· optional</span></Label>
<p className="text-xs text-gray-500">Drop your crew on the map so locals can find you. Tap the button — no typing needed.</p>
<div className="grid grid-cols-2 gap-4">
<Input placeholder="Latitude" value={latitude} onChange={(e) => setLatitude(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder="Longitude" value={longitude} onChange={(e) => setLongitude(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<Button type="button" variant="outline" size="sm" onClick={getCurrentLocation} disabled={isGettingLocation} className="mt-2 border-gray-700">
{isGettingLocation ? <><Loader2 className="h-4 w-4 mr-2 animate-spin" />Getting...</> : <><MapPin className="h-4 w-4 mr-2" />Use current location</>}
</Button>
{existing?.geohash && !latitude && (
<p className="text-xs text-gray-500">Leave blank to keep the current home turf location.</p>
)}
</div>
</>
);
const vibeBlock = (
<>
{/* Vibe tags */} {/* Vibe tags */}
<div className="space-y-3"> <div className="space-y-3">
<Label>Vibe / Focus</Label> <Label>Vibe / Focus</Label>
@ -571,8 +627,10 @@ export function CrewForm({ existing }: CrewFormProps) {
)} )}
</div> </div>
</div> </div>
</>
);
{/* Lab scene focus — cross-links the crew into The Lab */} const crewTypeBlock = (
<div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4"> <div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
<div> <div>
<Label className="flex items-center gap-2 text-teal-400"> <Label className="flex items-center gap-2 text-teal-400">
@ -615,8 +673,9 @@ export function CrewForm({ existing }: CrewFormProps) {
})} })}
</div> </div>
</div> </div>
);
{/* Crew AI agent (Sasquatch) */} const agentBlock = (
<div className="space-y-3 rounded-lg border border-teal-500/30 bg-teal-500/[0.04] p-4"> <div className="space-y-3 rounded-lg border border-teal-500/30 bg-teal-500/[0.04] p-4">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
@ -650,8 +709,9 @@ export function CrewForm({ existing }: CrewFormProps) {
</button> </button>
</div> </div>
</div> </div>
);
{/* Open call — looking for (crew ↔ Lab hiring pipeline) */} const openCallBlock = (
<div className="space-y-3 rounded-lg border border-fuchsia-500/30 bg-fuchsia-500/[0.04] p-4"> <div className="space-y-3 rounded-lg border border-fuchsia-500/30 bg-fuchsia-500/[0.04] p-4">
<div> <div>
<Label className="flex items-center gap-2 text-fuchsia-400"> <Label className="flex items-center gap-2 text-fuchsia-400">
@ -684,9 +744,9 @@ export function CrewForm({ existing }: CrewFormProps) {
})} })}
</div> </div>
</div> </div>
);
{/* Crew treasury (NIP-75 zap goal) — hidden for now via SHOW_CREW_TREASURY */} const treasuryBlock = SHOW_CREW_TREASURY ? (
{SHOW_CREW_TREASURY && (
<div className="space-y-3 rounded-lg border border-orange-500/30 bg-orange-500/[0.04] p-4"> <div className="space-y-3 rounded-lg border border-orange-500/30 bg-orange-500/[0.04] p-4">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
@ -749,9 +809,9 @@ export function CrewForm({ existing }: CrewFormProps) {
</div> </div>
)} )}
</div> </div>
)} ) : null;
{/* Members */} const membersBlock = (
<div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4"> <div className="space-y-3 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
<div> <div>
<Label className="flex items-center gap-2 text-teal-400"><Users className="h-4 w-4" /> Members</Label> <Label className="flex items-center gap-2 text-teal-400"><Users className="h-4 w-4" /> Members</Label>
@ -838,8 +898,9 @@ export function CrewForm({ existing }: CrewFormProps) {
</div> </div>
)} )}
</div> </div>
);
{/* Home spots */} const spotsBlock = (
<SpotPicker <SpotPicker
selectedSpots={selectedSpots} selectedSpots={selectedSpots}
onSpotsChange={setSelectedSpots} onSpotsChange={setSelectedSpots}
@ -847,11 +908,11 @@ export function CrewForm({ existing }: CrewFormProps) {
label="Home Spots" label="Home Spots"
description="Link the spots your crew haunts — they'll connect to your crew on the map" description="Link the spots your crew haunts — they'll connect to your crew on the map"
/> />
);
{/* Linked Lab profiles (cross-link) */} const labLinkBlock = <LabLinkField value={labRefs} onChange={setLabRefs} />;
<LabLinkField value={labRefs} onChange={setLabRefs} />
{/* The Part — crew video / montage */} const videoBlock = (
<div className="space-y-2 rounded-lg border border-teal-500/30 bg-teal-500/[0.04] p-4"> <div className="space-y-2 rounded-lg border border-teal-500/30 bg-teal-500/[0.04] p-4">
<Label htmlFor="crew-video" className="flex items-center gap-2 text-teal-400"> <Label htmlFor="crew-video" className="flex items-center gap-2 text-teal-400">
<Film className="h-4 w-4" /> The Part <span className="font-normal text-gray-500">· optional</span> <Film className="h-4 w-4" /> The Part <span className="font-normal text-gray-500">· optional</span>
@ -883,8 +944,9 @@ export function CrewForm({ existing }: CrewFormProps) {
) )
)} )}
</div> </div>
);
{/* Sponsor shop */} const sponsorBlock = (
<div className="space-y-2 rounded-lg border border-amber-500/30 bg-amber-500/[0.04] p-4"> <div className="space-y-2 rounded-lg border border-amber-500/30 bg-amber-500/[0.04] p-4">
<Label className="flex items-center gap-2 text-amber-400"> <Label className="flex items-center gap-2 text-amber-400">
<Store className="h-4 w-4" /> Sponsored by <span className="font-normal text-gray-500">· optional</span> <Store className="h-4 w-4" /> Sponsored by <span className="font-normal text-gray-500">· optional</span>
@ -895,16 +957,17 @@ export function CrewForm({ existing }: CrewFormProps) {
</p> </p>
<CrewSponsorPicker value={sponsorRef} onChange={setSponsorRef} /> <CrewSponsorPicker value={sponsorRef} onChange={setSponsorRef} />
</div> </div>
);
{/* Encrypted chat coordinator (Cordn / MLS) */} const coordinatorBlock = (
<div className="space-y-2 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.04] p-4"> <div className="space-y-2 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.04] p-4">
<Label htmlFor="crew-coordinator" className="flex items-center gap-2 text-emerald-400"> <Label htmlFor="crew-coordinator" className="flex items-center gap-2 text-emerald-400">
<Lock className="h-4 w-4" /> Private Chat Coordinator <span className="font-normal text-gray-500">· optional</span> <Lock className="h-4 w-4" /> Private Chat Coordinator <span className="font-normal text-gray-500">· advanced</span>
</Label> </Label>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Point your crew at a <span className="font-semibold text-emerald-300">Cordn</span> coordinator Most crews can skip this. If you run a <span className="font-semibold text-emerald-300">Cordn</span> coordinator
(an MLS delivery service reached over ContextVM) to enable end-to-end encrypted group chat. (an MLS delivery service reached over ContextVM), paste its URL to enable end-to-end encrypted group chat —
Nothing crew-specific ever hits a public relay. Paste the coordinator's URL. nothing crew-specific ever hits a public relay. Leave blank and your crew still gets a private group chat.
</p> </p>
<Input <Input
id="crew-coordinator" id="crew-coordinator"
@ -914,33 +977,9 @@ export function CrewForm({ existing }: CrewFormProps) {
className="bg-gray-800 border-gray-700 font-mono text-xs" className="bg-gray-800 border-gray-700 font-mono text-xs"
/> />
</div> </div>
);
<div className="grid grid-cols-2 gap-4"> const nip05Block = (
<div className="space-y-2">
<Label htmlFor="crew-location">Home Turf (City / Area)</Label>
<Input id="crew-location" placeholder="Bristol, UK" value={location} onChange={(e) => setLocation(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="crew-founded">Founded (Year)</Label>
<Input id="crew-founded" type="number" placeholder="2015" value={founded} onChange={(e) => setFounded(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
</div>
<div className="space-y-2">
<Label>Home Turf Coordinates (map anchor)</Label>
<div className="grid grid-cols-2 gap-4">
<Input placeholder="Latitude" value={latitude} onChange={(e) => setLatitude(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder="Longitude" value={longitude} onChange={(e) => setLongitude(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<Button type="button" variant="outline" size="sm" onClick={getCurrentLocation} disabled={isGettingLocation} className="mt-2 border-gray-700">
{isGettingLocation ? <><Loader2 className="h-4 w-4 mr-2 animate-spin" />Getting...</> : <><MapPin className="h-4 w-4 mr-2" />Use current location</>}
</Button>
{existing?.geohash && !latitude && (
<p className="text-xs text-gray-500">Leave blank to keep the current home turf location.</p>
)}
</div>
{/* Crew NIP-05 identity */}
<div className="space-y-2 rounded-lg border border-gray-700 bg-gray-900/60 p-4"> <div className="space-y-2 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
<Label htmlFor="crew-nip05" className="flex items-center gap-2 text-teal-400"> <Label htmlFor="crew-nip05" className="flex items-center gap-2 text-teal-400">
<AtSign className="h-4 w-4" /> Crew NIP-05 <span className="font-normal text-gray-500">· optional</span> <AtSign className="h-4 w-4" /> Crew NIP-05 <span className="font-normal text-gray-500">· optional</span>
@ -959,8 +998,9 @@ export function CrewForm({ existing }: CrewFormProps) {
className="bg-gray-800 border-gray-700 font-mono text-sm" className="bg-gray-800 border-gray-700 font-mono text-sm"
/> />
</div> </div>
);
{/* Links */} const linksBlock = (
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-base font-bold">Links</Label> <Label className="text-base font-bold">Links</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@ -1011,10 +1051,9 @@ export function CrewForm({ existing }: CrewFormProps) {
</div> </div>
<NofferField value={noffer} onChange={setNoffer} /> <NofferField value={noffer} onChange={setNoffer} />
</div> </div>
);
{/* Crew Band — many crews double as local bands. Optional. const bandBlock = (
Interconnects with the Lab: a serious band can set its Crew Type to
"Band & Music" and link a full Musician profile below. */}
<div className="space-y-3 rounded-xl border border-purple-500/20 bg-purple-500/[0.04] p-4"> <div className="space-y-3 rounded-xl border border-purple-500/20 bg-purple-500/[0.04] p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Music className="h-4 w-4 text-purple-400" /> <Music className="h-4 w-4 text-purple-400" />
@ -1072,34 +1111,211 @@ export function CrewForm({ existing }: CrewFormProps) {
<strong className="text-gray-300">Linked Lab profiles</strong> section above. <strong className="text-gray-300">Linked Lab profiles</strong> section above.
</p> </p>
</div> </div>
);
{/* Logo */} const busy = isPublishing || isUploading;
<div className="space-y-2">
<Label>Crew Logo / Photo</Label> // ─── Edit mode: flat single page (all sections at once) ───────────────────
{image ? ( if (isEditing) {
<div className="relative w-32 h-32 rounded-2xl overflow-hidden bg-gray-800"> return (
<img src={image} alt="Crew" className="w-full h-full object-cover" /> <form onSubmit={handleSubmit}>
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500"> <Card className="bg-gray-900 border-gray-700">
<X className="h-4 w-4" /> <CardHeader>
</button> <CardTitle className="flex items-center gap-2">
</div> <Users className="h-5 w-5 text-teal-400" /> Edit Crew
) : ( </CardTitle>
<label className="w-32 h-32 rounded-2xl border-2 border-dashed border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-800/50"> <CardDescription className="text-gray-400">Your crew's identity, home turf and members</CardDescription>
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} disabled={isUploading} /> </CardHeader>
{isUploading ? <Loader2 className="h-6 w-6 animate-spin" /> : <><Upload className="h-6 w-6 mb-1 text-gray-500" /><span className="text-xs text-gray-500">Upload</span></>} <CardContent className="space-y-6">
</label> {logoBlock}
)} {nameBlock}
{bioBlock}
{vibeBlock}
{crewTypeBlock}
{agentBlock}
{openCallBlock}
{treasuryBlock}
{membersBlock}
{spotsBlock}
{labLinkBlock}
{videoBlock}
{sponsorBlock}
{coordinatorBlock}
{locationBlock}
{nip05Block}
{linksBlock}
{bandBlock}
<Button type="submit" className={cn('w-full uppercase font-black', TEAL)} disabled={busy}>
{isPublishing ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
) : (
<><Users className="h-4 w-4 mr-2" />Save Crew</>
)}
</Button>
</CardContent>
</Card>
</form>
);
}
// ─── Create mode: guided 4-step wizard ────────────────────────────────────
const steps: { title: string; hint: string; content: ReactNode }[] = [
{
title: 'The Basics',
hint: 'Just the essentials — you can spin up a crew with only this step.',
content: (
<div className="space-y-6">
<div className="flex flex-col items-center gap-3 text-center sm:flex-row sm:items-start sm:text-left">
{logoBlock}
<p className="max-w-xs text-xs text-gray-500 sm:pt-8">
A logo makes your crew instantly recognisable on the map and in feeds. Square works best.
</p>
</div> </div>
{nameBlock}
{bioBlock}
{locationBlock}
</div>
),
},
{
title: 'Identity & Vibe',
hint: 'How your crew shows up around the app. All optional.',
content: (
<div className="space-y-6">
{vibeBlock}
{crewTypeBlock}
</div>
),
},
{
title: 'Squad & Spots',
hint: 'Add your people and your home turf. You can always do this later.',
content: (
<div className="space-y-6">
{membersBlock}
{spotsBlock}
{openCallBlock}
</div>
),
},
{
title: 'Extras',
hint: 'Power-ups for later — skip anything you’re unsure about.',
content: (
<div className="space-y-6">
{treasuryBlock}
{videoBlock}
{sponsorBlock}
{labLinkBlock}
{linksBlock}
{bandBlock}
{nip05Block}
{agentBlock}
{coordinatorBlock}
</div>
),
},
];
<Button type="submit" className={cn('w-full uppercase font-black', TEAL)} disabled={isPublishing || isUploading}> const lastStep = steps.length - 1;
const current = steps[step];
const goNext = () => {
// Only gate on the one truly-required field, at the step it lives on.
if (step === 0 && !name.trim()) {
toast({ title: 'Name your crew', description: 'A crew needs a name before you continue.', variant: 'destructive' });
return;
}
setStep((s) => Math.min(s + 1, lastStep));
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const goBack = () => {
setStep((s) => Math.max(s - 1, 0));
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<form onSubmit={handleSubmit}>
{/* Progress header */}
<div className="mb-5">
<div className="mb-2 flex items-center justify-between">
<span className="text-[11px] font-black uppercase tracking-widest text-teal-400">
Step {step + 1} of {steps.length}
</span>
<span className="text-[11px] font-bold text-gray-500">{current.title}</span>
</div>
<div className="flex gap-1.5">
{steps.map((s, i) => (
<button
key={s.title}
type="button"
onClick={() => {
// Allow jumping back freely; jumping forward requires a name.
if (i > step && !name.trim()) {
toast({ title: 'Name your crew first', variant: 'destructive' });
return;
}
setStep(i);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className={cn(
'h-1.5 flex-1 rounded-full transition-colors',
i <= step ? 'bg-teal-500' : 'bg-gray-700 hover:bg-gray-600',
)}
aria-label={`Go to step ${i + 1}: ${s.title}`}
/>
))}
</div>
</div>
<Card className="bg-gray-900 border-gray-700">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5 text-teal-400" /> {current.title}
</CardTitle>
<CardDescription className="text-gray-400">{current.hint}</CardDescription>
</CardHeader>
<CardContent>{current.content}</CardContent>
</Card>
{/* Sticky footer nav */}
<div className="sticky bottom-0 z-10 mt-4 flex items-center gap-3 rounded-2xl border border-gray-700 bg-gray-900/95 p-3 backdrop-blur supports-[backdrop-filter]:bg-gray-900/80">
{step > 0 ? (
<Button type="button" variant="outline" onClick={goBack} disabled={busy} className="border-gray-700">
Back
</Button>
) : (
<span className="flex-1" />
)}
{step < lastStep ? (
<>
{step > 0 && (
<Button
type="button"
variant="ghost"
onClick={() => setStep(lastStep)}
disabled={busy}
className="text-gray-400 hover:text-white"
>
Skip to end
</Button>
)}
<Button type="button" onClick={goNext} disabled={busy} className={cn('flex-1 font-black uppercase', TEAL)}>
Continue
</Button>
</>
) : (
<Button type="submit" disabled={busy} className={cn('flex-1 font-black uppercase', TEAL)}>
{isPublishing ? ( {isPublishing ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</> <><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
) : ( ) : (
<><Users className="h-4 w-4 mr-2" />{existing ? 'Save Crew' : 'Create Crew'}</> <><Users className="h-4 w-4 mr-2" />Create Crew</>
)} )}
</Button> </Button>
</CardContent> )}
</Card> </div>
</form> </form>
); );
} }

View file

@ -43,7 +43,7 @@ export default function CreateCrewPage() {
<h1 className="text-3xl font-black uppercase text-teal-400" style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}> <h1 className="text-3xl font-black uppercase text-teal-400" style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}>
Create a Crew Create a Crew
</h1> </h1>
<p className="text-gray-400 mt-1">Your skate family — the people you skate with. Plant your flag on the map.</p> <p className="text-gray-400 mt-1">Your skate family — the people you skate with. Just a name gets you started; everything else is optional.</p>
</div> </div>
<CrewForm /> <CrewForm />
</div> </div>