SkateSpots/src/pages/BecomeBuilderPage.tsx

751 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useNavigate, Link } from 'react-router-dom';
import ngeohash from 'ngeohash';
import { nip19 } from 'nostr-tools';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast';
import { useWallet } from '@/hooks/useWallet';
import { WalletModal } from '@/components/WalletModal';
import { LoginArea } from '@/components/auth/LoginArea';
import { Nip05Required } from '@/components/Nip05Required';
import { SpotPicker } from '@/components/SpotPicker';
import { NofferField } from '@/components/NofferField';
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
import { EntityTypeField } from '@/components/EntityTypeField';
import { CrewLinkField } from '@/components/CrewLinkField';
import {
SKATE_BUILDER_KIND,
SKATE_SPOT_KIND,
BUILDER_SERVICE_LABELS,
BUILDER_SERVICE_DESCRIPTIONS,
BUILDER_SKILL_LABELS,
BUILDER_SCALE_LABELS,
isValidNoffer,
type EntityType,
} from '@/lib/types';
import {
BUILDER_SERVICE_KEYS,
BUILDER_SERVICE_DESC_KEYS,
BUILDER_SKILL_KEYS,
BUILDER_SCALE_KEYS,
} from '@/lib/labelKeys';
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
import { useTranslation } from '@/contexts/language';
import { cn } from '@/lib/utils';
import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Hammer, Eye } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
import { LabExtrasFields } from '@/components/LabExtrasFields';
import { LabProfilePreview } from '@/components/LabProfilePreview';
import { LabDraftBanner } from '@/components/LabDraftBanner';
import { LabProfileProgress } from '@/components/LabProfileProgress';
import { useLabDraft } from '@/hooks/useLabDraft';
import { labProfilePath } from '@/lib/labRoutes';
import { pickRateUnits } from '@/lib/labExtras';
const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS);
const SKILL_ENTRIES = Object.entries(BUILDER_SKILL_LABELS);
const SCALE_ENTRIES = Object.entries(BUILDER_SCALE_LABELS);
function ChipGrid({
entries,
selected,
onToggle,
keys,
}: {
entries: [string, string][];
selected: string[];
onToggle: (key: string) => void;
keys?: Record<string, string>;
}) {
const { t } = useTranslation();
return (
<div className="flex flex-wrap gap-2">
{entries.map(([key, label]) => (
<button
key={key}
type="button"
onClick={() => onToggle(key)}
className={cn(
'px-3 py-2 rounded-xl text-xs font-bold border transition-all active:scale-95',
selected.includes(key)
? 'bg-amber-500 text-black border-amber-500'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700 text-gray-300'
)}
>
{keys?.[key] ? t(keys[key]) : label}
</button>
))}
</div>
);
}
export default function BecomeBuilderPage() {
const { t } = useTranslation();
useSeoMeta({
title: t('seoUi.becomeBuilderTitle'),
description: t('seoUi.becomeBuilderDesc'),
});
const navigate = useNavigate();
const { user, metadata } = useCurrentUser();
const author = useAuthor(user?.pubkey || '');
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const { toast } = useToast();
const { webln, activeNWC } = useWallet();
const hasWallet = !!webln || !!activeNWC;
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
const [latitude, setLatitude] = useState('');
const [longitude, setLongitude] = useState('');
const [selectedServices, setSelectedServices] = useState<string[]>([]);
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const [selectedScale, setSelectedScale] = useState<string[]>([]);
const [gear, setGear] = useState('');
const [serviceArea, setServiceArea] = useState('');
const [insured, setInsured] = useState(false);
const [image, setImage] = useState<string | null>(null);
const [experience, setExperience] = useState('');
const [rate, setRate] = useState('');
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [website, setWebsite] = useState('');
const [portfolio, setPortfolio] = useState('');
const [social, setSocial] = useState('');
const [noffer, setNoffer] = useState('');
const [entityType, setEntityType] = useState<EntityType>('collective');
const [members, setMembers] = useState<string[]>([]);
const [crewRef, setCrewRef] = useState<string | undefined>(undefined);
const [selectedSpots, setSelectedSpots] = useState<string[]>([]);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [titleInput, setTitleInput] = useState('');
const [prefilled, setPrefilled] = useState(false);
const [extraTags, setExtraTags] = useState<string[][]>([]);
const [previewOpen, setPreviewOpen] = useState(false);
const [lead_time, setLeadTime] = useState('');
const [warranty, setWarranty] = useState('');
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
// Prefill from an existing profile so this is an edit, not a blind overwrite.
const { data: existingProfile } = useMyLabProfileEvent(SKATE_BUILDER_KIND, 'builder-profile');
const draft = useLabDraft(
`craft:builder`,
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, selectedScale, gear, serviceArea, insured, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, selectedSpots, lead_time, warranty, extraTags },
(d) => {
setTitleInput(d.titleInput);
setBio(d.bio);
setLocation(d.location);
setLatitude(d.latitude);
setLongitude(d.longitude);
setSelectedServices(d.selectedServices);
setSelectedSkills(d.selectedSkills);
setSelectedScale(d.selectedScale);
setGear(d.gear);
setServiceArea(d.serviceArea);
setInsured(d.insured);
setImage(d.image);
setExperience(d.experience);
setRate(d.rate);
setPhone(d.phone);
setEmail(d.email);
setWebsite(d.website);
setPortfolio(d.portfolio);
setSocial(d.social);
setNoffer(d.noffer);
setEntityType(d.entityType);
setMembers(d.members);
setCrewRef(d.crewRef);
setSelectedSpots(d.selectedSpots);
setLeadTime(d.lead_time);
setWarranty(d.warranty);
setExtraTags(d.extraTags);
},
);
useEffect(() => {
if (prefilled || !existingProfile) return;
setPrefilled(true);
const tag = (n: string) => existingProfile.tags.find(([t]) => t === n)?.[1];
const tags = (n: string) => existingProfile.tags.filter(([t]) => t === n).map(([, v]) => v);
const keys = (m: Record<string, string>) => new Set(Object.keys(m));
const svcK = keys(BUILDER_SERVICE_LABELS);
const skillK = keys(BUILDER_SKILL_LABELS);
const scaleK = keys(BUILDER_SCALE_LABELS);
const sel = tags('t').filter((v) => v !== 'skatebuilder');
setTitleInput(tag('title') ?? '');
setExistingGeohash(tag('g'));
setGoPro(existingProfile.tags.some(([t]) => t === 'featured'));
setBio(existingProfile.content ?? '');
setLocation(tag('location') ?? '');
setImage(tag('image') ?? null);
setExperience(tag('experience') ?? '');
setRate(tag('rate') ?? '');
setLeadTime(tag('lead_time') ?? '');
setWarranty(tag('warranty') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
setPortfolio(tag('portfolio') ?? '');
setSocial(tag('social') ?? '');
setNoffer(tag('noffer') ?? '');
setGear(tag('gear') ?? '');
setServiceArea(tag('service_area') ?? '');
setInsured(tag('insured') === 'true');
setEntityType((tag('entity') as EntityType) ?? 'collective');
setMembers(tags('p'));
setCrewRef(existingProfile.tags.find(([t, v]) => t === 'a' && v.startsWith('35006:'))?.[1]);
setSelectedServices(sel.filter((v) => svcK.has(v)));
setSelectedSkills(sel.filter((v) => skillK.has(v)));
setSelectedScale(sel.filter((v) => scaleK.has(v)));
setSelectedSpots(
existingProfile.tags
.filter(([t, v]) => t === 'a' && v.startsWith(`${SKATE_SPOT_KIND}:`))
.map(([, v]) => {
const [, pk, ...rest] = v.split(':');
return `${pk}:${rest.join(':')}`;
}),
);
}, [existingProfile, prefilled]);
const getDisplayName = () => {
if (author.data?.metadata?.name) return author.data.metadata.name;
if (user?.pubkey) {
const npub = nip19.npubEncode(user.pubkey);
return `${npub.slice(0, 12)}...${npub.slice(-4)}`;
}
return t('labUi.builderForm.anonymous');
};
const toggle = (setter: React.Dispatch<React.SetStateAction<string[]>>) => (key: string) => {
setter((prev) => (prev.includes(key) ? prev.filter((s) => s !== key) : [...prev, key]));
};
const getCurrentLocation = () => {
if (!navigator.geolocation) {
toast({ title: t('labUi.become.geolocationUnsupported'), variant: 'destructive' });
return;
}
setIsGettingLocation(true);
navigator.geolocation.getCurrentPosition(
(position) => {
setLatitude(position.coords.latitude.toFixed(6));
setLongitude(position.coords.longitude.toFixed(6));
setIsGettingLocation(false);
toast({ title: t('labUi.become.locationFound') });
},
(error) => {
setIsGettingLocation(false);
toast({ title: t('labUi.become.locationError'), description: error.message, variant: 'destructive' });
}
);
};
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
try {
const file = files[0];
const tags = await uploadFile(file);
const url = tags[0]?.[1];
if (url) {
setImage(url);
toast({ title: t('labUi.become.imageUploaded') });
}
} catch (error) {
toast({ title: t('labUi.become.uploadFailed'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
}
e.target.value = '';
};
// Optional, but if given it must be usable — a bad email/URL would otherwise
// be stored and then silently hidden.
const contactError = email.trim() && !isValidEmail(email)
? t('labUi.become.invalidEmail')
: website.trim() && !sanitizeUrl(normalizeUrlInput(website))
? t('labUi.become.invalidWebsite')
: portfolio.trim() && !sanitizeUrl(normalizeUrlInput(portfolio))
? t('labUi.become.invalidPortfolio')
: '';
const handleSubmit = async (e: React.FormEvent) => {
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;
}
try {
const identifier = 'builder-profile';
const displayName = getDisplayName();
const tags: string[][] = [
['d', identifier],
['title', titleInput.trim() || displayName],
['t', 'skatebuilder'],
['alt', `Skate ramp/park builder: ${displayName}`],
];
if (location.trim()) tags.push(['location', location.trim()]);
if (latitude && longitude) {
const lat = parseFloat(latitude);
const lng = parseFloat(longitude);
if (!isNaN(lat) && !isNaN(lng)) {
const geohash = ngeohash.encode(lat, lng, 7);
tags.push(['g', geohash]);
}
} else if (existingGeohash) {
// Preserve the existing map pin when editing without re-locating.
tags.push(['g', existingGeohash]);
}
for (const s of [...selectedServices, ...selectedSkills, ...selectedScale]) {
tags.push(['t', s]);
}
if (gear.trim()) tags.push(['gear', gear.trim()]);
if (serviceArea.trim()) tags.push(['service_area', serviceArea.trim()]);
if (insured) tags.push(['insured', 'true']);
if (image) tags.push(['image', image]);
if (experience.trim()) tags.push(['experience', experience.trim()]);
if (rate.trim()) tags.push(['rate', rate.trim()]);
if (phone.trim()) tags.push(['phone', phone.trim()]);
if (email.trim()) tags.push(['email', email.trim()]);
if (website.trim()) tags.push(['website', normalizeUrlInput(website)]);
if (portfolio.trim()) tags.push(['portfolio', normalizeUrlInput(portfolio)]);
if (social.trim()) tags.push(['social', normalizeUrlInput(social)]);
if (isValidNoffer(noffer.trim())) tags.push(['noffer', noffer.trim().toLowerCase()]);
tags.push(['entity', entityType]);
for (const extra of extraTags) tags.push(extra);
if (lead_time.trim()) tags.push(['lead_time', lead_time.trim()]);
if (warranty.trim()) tags.push(['warranty', warranty.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
}
for (const spotId of selectedSpots) {
const [spotPubkey, spotIdentifier] = spotId.split(':');
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
}
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({
kind: SKATE_BUILDER_KIND,
content: bio.trim(),
tags,
});
draft.clear();
toast({ title: existingProfile ? t('labUi.builderForm.updated') : t('labUi.builderForm.created'), description: t('labUi.become.profilePublished') });
navigate(`${labProfilePath('builder', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
} catch (error) {
toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' });
}
};
if (!user) {
return (
<>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto bg-gray-900 border-gray-700">
<CardContent className="py-12 text-center">
<HardHat className="h-12 w-12 mx-auto mb-4 text-gray-600" />
<h2 className="text-xl font-bold mb-2">{t('labUi.become.loginRequired')}</h2>
<p className="text-gray-400 mb-6">{t('labUi.become.loginRequiredDesc', { role: t('labUi.role.builder') })}</p>
<LoginArea className="justify-center max-w-60 mx-auto" />
</CardContent>
</Card>
</div>
</>
);
}
const hasNip05 = metadata?.nip05;
return (
<>
<div className="container px-4 sm:px-6 py-6 sm:py-8 max-w-2xl">
<div className="mb-6">
<Link to="/lab" className="inline-flex items-center text-gray-400 hover:text-white transition-colors mb-4">
<ArrowLeft className="h-4 w-4 mr-2" />
{t('labUi.become.backToLab')}
</Link>
<h1 className="text-3xl font-black uppercase text-amber-400" style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}>
{t('labUi.builderForm.heading')}
</h1>
<p className="text-gray-400 mt-1">{t('labUi.builderForm.tagline')}</p>
{!hasNip05 && (
<Nip05Required
variant="banner"
color="#eab308"
role="builders"
reason={t('labUi.builderForm.nip05Reason')}
className="mt-4"
/>
)}
{/* Wallet Requirement */}
<Card className="mt-4 bg-amber-950/30 border-2 border-amber-500/50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Wallet className="h-6 w-6 text-amber-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="font-bold text-amber-400 mb-2">{t('labUi.become.paymentSetup')}</h3>
<p className="text-sm text-gray-300 mb-3">
{t('labUi.builderForm.paymentDesc')}
</p>
{hasWallet ? (
<div className="p-3 rounded-lg bg-green-950/30 border border-green-500/50">
<div className="flex items-center gap-2 mb-1">
<div className="h-2 w-2 rounded-full bg-green-500" />
<p className="text-sm font-bold text-green-400">{t('labUi.become.walletConnected')}</p>
</div>
<p className="text-xs text-gray-400">{webln ? t('labUi.become.usingWebln') : t('labUi.become.usingNwc')}</p>
</div>
) : (
<div className="space-y-3">
<div className="p-3 rounded-lg bg-red-950/30 border border-red-500/50">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-red-500" />
<p className="text-sm font-bold text-red-400">{t('labUi.become.noWallet')}</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<WalletModal>
<Button className="gap-2 bg-amber-500 hover:bg-amber-600 text-black flex-1">
<Wallet className="h-4 w-4" />
{t('labUi.become.connectWalletNwc')}
</Button>
</WalletModal>
<a href="https://planb.academy/en/tutorials/wallet" target="_blank" rel="noopener noreferrer" className="flex-1">
<Button variant="outline" className="gap-2 w-full border-amber-500/50 text-amber-400 hover:bg-amber-500/10">
<ExternalLink className="h-4 w-4" />
{t('labUi.become.learnHow')}
</Button>
</a>
</div>
<p className="text-xs text-gray-400 italic">
{t('labUi.become.tip')}
</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
<LabProfileProgress
hasPhoto={!!image}
hasBio={bio.trim().length > 20}
hasSkills={selectedServices.length + selectedSkills.length + selectedScale.length > 0}
hasRate={!!rate.trim()}
hasContact={!!(email || website || phone || social)}
hasLocation={!!location.trim()}
/>
<form onSubmit={handleSubmit}>
<Card className="bg-gray-900 border-gray-700">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardHat className="h-5 w-5 text-amber-400" /> {t('labUi.builderForm.profileTitle')}
</CardTitle>
<CardDescription className="text-gray-400">{t('labUi.builderForm.profileDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>{t('labUi.builderForm.identity')}</Label>
<div className="p-4 rounded-lg border-2 border-amber-500/30 bg-amber-500/5">
<p className="font-black text-lg">{getDisplayName()}</p>
<p className="text-sm text-gray-400 mt-1">{t('labUi.become.fromNostrProfile')}</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">{t('labUi.become.headline')}</Label>
<Input
id="title"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
placeholder={getDisplayName()}
maxLength={100}
className="bg-gray-800 border-gray-700"
/>
<p className="text-xs text-gray-500">
{t('labUi.become.headlineHint', { example: t('labUi.builderForm.headlineExample') })}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="bio">{t('labUi.builderForm.bioLabel')}</Label>
<Textarea id="bio" placeholder={t('labUi.builderForm.bioPlaceholder')} value={bio} onChange={(e) => setBio(e.target.value)} rows={5} maxLength={3000} className="bg-gray-800 border-gray-700" />
<p className="text-right text-xs text-gray-500">{bio.length}/3000</p>
</div>
{/* Individual vs Collective — defaults to collective for builders */}
<EntityTypeField
entityType={entityType}
onEntityTypeChange={setEntityType}
members={members}
onMembersChange={setMembers}
accentBg="bg-amber-500 hover:bg-amber-600 text-black"
accentText="text-amber-400"
/>
{entityType === 'collective' && (
<CrewLinkField value={crewRef} onChange={setCrewRef} accentBg="bg-amber-500 hover:bg-amber-600 text-black" accentText="text-amber-400" />
)}
{/* Build types */}
<div className="space-y-3">
<Label>{t('labUi.builderForm.buildTypesLabel')}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{SERVICE_ENTRIES.map(([service, label]) => (
<button
key={service}
type="button"
onClick={() => toggle(setSelectedServices)(service)}
className={cn(
'p-3 rounded-lg text-left transition-all border text-sm',
selectedServices.includes(service)
? 'bg-amber-500 text-black border-amber-500'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700'
)}
>
<div className="font-bold">{BUILDER_SERVICE_KEYS[service] ? t(BUILDER_SERVICE_KEYS[service]) : label}</div>
<div className={cn('text-xs mt-1 line-clamp-1', selectedServices.includes(service) ? 'text-black/70' : 'text-gray-400')}>
{BUILDER_SERVICE_DESC_KEYS[service] ? t(BUILDER_SERVICE_DESC_KEYS[service]) : BUILDER_SERVICE_DESCRIPTIONS[service]}
</div>
</button>
))}
</div>
</div>
{/* Materials / trade skills */}
<div className="space-y-3">
<Label className="flex items-center gap-2"><Hammer className="h-4 w-4 text-amber-400" /> {t('labUi.builderForm.materialsLabel')}</Label>
<ChipGrid entries={SKILL_ENTRIES} keys={BUILDER_SKILL_KEYS} selected={selectedSkills} onToggle={toggle(setSelectedSkills)} />
</div>
{/* Scale */}
<div className="space-y-3">
<Label className="flex items-center gap-2">📐 {t('labUi.builderForm.scaleLabel')}</Label>
<ChipGrid entries={SCALE_ENTRIES} keys={BUILDER_SCALE_KEYS} selected={selectedScale} onToggle={toggle(setSelectedScale)} />
</div>
{/* Freeform "how we work" */}
<div className="space-y-2">
<Label htmlFor="gear">{t('labUi.builderForm.gearLabel')}</Label>
<Textarea
id="gear"
placeholder={t('labUi.builderForm.gearPlaceholder')}
value={gear}
onChange={(e) => setGear(e.target.value)}
rows={3}
className="bg-gray-800 border-gray-700"
/>
<p className="text-xs text-gray-500">{t('labUi.builderForm.gearHelper')}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="experience">{t('labUi.builderForm.yearsBuilding')}</Label>
<Input id="experience" type="number" placeholder="10" value={experience} onChange={(e) => setExperience(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="rate">{t('labUi.builderForm.rateLabel')}</Label>
<Input id="rate" placeholder={t('labUi.builderForm.ratePlaceholder')} value={rate} onChange={(e) => setRate(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="serviceArea">{t('labUi.builderForm.serviceArea')}</Label>
<Input id="serviceArea" placeholder={t('labUi.builderForm.serviceAreaPlaceholder')} value={serviceArea} onChange={(e) => setServiceArea(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="flex items-center justify-between rounded-lg border border-gray-700 bg-gray-800/60 p-3">
<div>
<Label htmlFor="insured" className="font-bold">{t('labUi.builderForm.insured')}</Label>
<p className="text-xs text-gray-500">{t('labUi.builderForm.insuredHelper')}</p>
</div>
<Switch id="insured" checked={insured} onCheckedChange={setInsured} />
</div>
<div className="space-y-2">
<Label htmlFor="location">{t('labUi.builderForm.cityBase')}</Label>
<Input id="location" placeholder="Malmö, SE" value={location} onChange={(e) => setLocation(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
{/* Contact */}
<div className="space-y-3">
<Label className="text-base font-bold">{t('labUi.builderForm.contactPortfolio')}</Label>
<p className="text-sm text-gray-400">{t('labUi.builderForm.contactPortfolioDesc')}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="portfolio">{t('labUi.builderForm.pastBuilds')}</Label>
<Input id="portfolio" type="url" placeholder="https://instagram.com/crew" value={portfolio} onChange={(e) => setPortfolio(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="website">{t('labUi.become.website')}</Label>
<Input id="website" type="url" placeholder="https://ourbuilds.com" value={website} onChange={(e) => setWebsite(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="phone">{t('labUi.become.phoneNumber')}</Label>
<Input id="phone" type="tel" placeholder="+1 234 567 8900" value={phone} onChange={(e) => setPhone(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="email">{t('labUi.become.email')}</Label>
<Input id="email" type="email" placeholder="crew@example.com" value={email} onChange={(e) => setEmail(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="social">{t('labUi.become.otherSocial')}</Label>
<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 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">
<p className="text-sm text-gray-300">{t('labUi.builderForm.dmNote')}</p>
</div>
</div>
{/* Spot Picker — spots they've built */}
<SpotPicker
selectedSpots={selectedSpots}
onSpotsChange={setSelectedSpots}
maxSpots={8}
label={t('labUi.builderForm.spotsLabel')}
description={t('labUi.builderForm.spotsDesc')}
/>
<div className="space-y-2">
<Label>{t('labUi.become.coordinatesOptional')}</Label>
<div className="grid grid-cols-2 gap-4">
<Input placeholder={t('labUi.become.latitude')} value={latitude} onChange={(e) => setLatitude(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder={t('labUi.become.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" />{t('labUi.become.getting')}</> : <><MapPin className="h-4 w-4 mr-2" />{t('labUi.become.useCurrentLocation')}</>}
</Button>
</div>
<div className="space-y-2">
<Label>{t('labUi.builderForm.logoPhoto')}</Label>
{image ? (
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-800">
<img src={image} alt={t('legacyMiscUi.altProfile')} 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-full border-2 border-dashed border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-800/50">
<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">{t('labUi.become.upload')}</span></>}
</label>
)}
</div>
{/* Optional Pro upgrade — free to list without it */}
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.leadTime')}</Label>
<Input value={lead_time} onChange={(e) => setLeadTime(e.target.value)} placeholder="e.g. 2–4 weeks" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.warranty')}</Label>
<Input value={warranty} onChange={(e) => setWarranty(e.target.value)} placeholder="e.g. 12 months on ramps" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}
accentText="text-amber-400"
accentChip="bg-amber-500 text-black border-amber-500"
accentChipHover="hover:border-amber-500/60 hover:text-amber-400"
rateUnits={pickRateUnits('project', 'day', 'hour')}
/>
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-amber-400" entityCoord={user ? `${SKATE_BUILDER_KIND}:${user.pubkey}:builder-profile` : undefined} />
<Button
type="button"
variant="outline"
onClick={() => setPreviewOpen(true)}
className="w-full h-10 gap-2 border-gray-700 font-bold"
>
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
</Button>
<Button
type="submit"
className="w-full bg-amber-500 hover:bg-amber-600 text-black uppercase font-black"
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />{t('labUi.become.publishing')}</>
) : (
<><HardHat className="h-4 w-4 mr-2" />{existingProfile ? t('labUi.builderForm.update') : isPro ? t('labUi.builderForm.createPro') : t('labUi.builderForm.create')}</>
)}
</Button>
</CardContent>
</Card>
</form>
</div>
<LabProfilePreview
open={previewOpen}
onOpenChange={setPreviewOpen}
heading={t('labUi.detail.noun.builder')}
noun={t('labUi.detail.noun.builder')}
accentText="text-amber-400"
accentChip="bg-amber-500 text-black border-amber-500"
icon={HardHat}
draft={{
title: titleInput.trim() || getDisplayName(),
bio, image, location, experience, rate,
extraTags,
contact: { phone, email, website, portfolio, social, noffer },
}}
/>
</>
);
}