SkateSpots/src/pages/BecomeFilmerPage.tsx

725 lines
34 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 { 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_FILMER_KIND, SKATE_SPOT_KIND, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
import { FILMING_SERVICE_KEYS, FILMING_SERVICE_DESC_KEYS, FILMING_SKILL_KEYS, FILMING_SKILL_DESC_KEYS } from '@/lib/labelKeys';
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
import { useTranslation } from '@/contexts/language';
import { cn } from '@/lib/utils';
import { Video, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, 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(FILMING_SERVICE_LABELS);
const SKILL_ENTRIES = Object.entries(FILMING_SKILL_LABELS);
export default function BecomeFilmerPage() {
const { t } = useTranslation();
useSeoMeta({
title: t('seoUi.becomeFilmerTitle'),
description: t('seoUi.becomeFilmerDesc'),
});
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();
// Check if user has any wallet connected
const hasWallet = !!webln || !!activeNWC;
// Optional Pro upgrade — free to list; Pro adds a badge + featured placement
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 [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>('individual');
const [members, setMembers] = useState<string[]>([]);
const [crewRef, setCrewRef] = useState<string | undefined>(undefined);
const [gear, setGear] = useState('');
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 [turnaround, setTurnaround] = useState('');
const [deliverables, setDeliverables] = useState('');
const [drone, setDrone] = 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_FILMER_KIND, 'filming-profile');
const draft = useLabDraft(
`craft:filmer`,
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, gear, selectedSpots, turnaround, deliverables, drone, extraTags },
(d) => {
setTitleInput(d.titleInput);
setBio(d.bio);
setLocation(d.location);
setLatitude(d.latitude);
setLongitude(d.longitude);
setSelectedServices(d.selectedServices);
setSelectedSkills(d.selectedSkills);
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);
setGear(d.gear);
setSelectedSpots(d.selectedSpots);
setTurnaround(d.turnaround);
setDeliverables(d.deliverables);
setDrone(d.drone);
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 serviceKeys = new Set(Object.keys(FILMING_SERVICE_LABELS));
const skillKeys = new Set(Object.keys(FILMING_SKILL_LABELS));
const sel = tags('t').filter((v) => v !== 'skatefilmer');
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') ?? '');
setTurnaround(tag('turnaround') ?? '');
setDeliverables(tag('deliverables') ?? '');
setDrone(tag('drone') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
setPortfolio(tag('portfolio') ?? '');
setSocial(tag('social') ?? '');
setNoffer(tag('noffer') ?? '');
setGear(tag('gear') ?? '');
setEntityType((tag('entity') as EntityType) ?? 'individual');
setMembers(tags('p'));
setCrewRef(existingProfile.tags.find(([t, v]) => t === 'a' && v.startsWith('35006:'))?.[1]);
setSelectedServices(sel.filter((v) => serviceKeys.has(v)));
setSelectedSkills(sel.filter((v) => skillKeys.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.filmerForm.anonymous');
};
const toggleService = (service: string) => {
setSelectedServices((prev) => prev.includes(service) ? prev.filter((s) => s !== service) : [...prev, service]);
};
const toggleSkill = (skill: string) => {
setSelectedSkills((prev) => prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill]);
};
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.filmerForm.servicesRequired'), description: t('labUi.filmerForm.selectService'), variant: 'destructive' });
return;
}
if (selectedSkills.length === 0) {
toast({ title: t('labUi.filmerForm.skillsRequired'), description: t('labUi.filmerForm.selectSkill'), variant: 'destructive' });
return;
}
try {
const identifier = 'filming-profile';
const displayName = getDisplayName();
const tags: string[][] = [
['d', identifier],
['title', titleInput.trim() || displayName],
['t', 'skatefilmer'],
['alt', `Skate filmer: ${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 service of selectedServices) tags.push(['t', service]);
for (const skill of selectedSkills) tags.push(['t', skill]);
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 (turnaround.trim()) tags.push(['turnaround', turnaround.trim()]);
if (deliverables.trim()) tags.push(['deliverables', deliverables.trim()]);
if (drone.trim()) tags.push(['drone', drone.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
}
if (gear.trim()) tags.push(['gear', gear.trim()]);
// Add spot references
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_FILMER_KIND,
content: bio.trim(),
tags,
});
draft.clear();
toast({ title: existingProfile ? t('labUi.filmerForm.updated') : t('labUi.filmerForm.created'), description: t('labUi.become.profilePublished') });
navigate(`${labProfilePath('filmer', 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">
<Video 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.filmer') })}</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 neon-lime" style={{
fontFamily: 'Impact, "Arial Black", sans-serif'
}}>{t('labUi.filmerForm.heading')}</h1>
<p className="text-gray-400 mt-1">{t('labUi.filmerForm.tagline')}</p>
{!hasNip05 && (
<Nip05Required
variant="banner"
color="#eab308"
role="filmers"
reason={t('labUi.filmerForm.nip05Reason')}
className="mt-4"
/>
)}
{/* Wallet Requirement */}
<Card className="mt-4 bg-orange-950/30 border-2 border-orange-500/50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Wallet className="h-6 w-6 text-orange-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="font-bold text-orange-400 mb-2">{t('labUi.become.paymentSetup')}</h3>
<p className="text-sm text-gray-300 mb-3">
{t('labUi.filmerForm.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-orange-500 hover:bg-orange-600 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-orange-500/50 text-orange-400 hover:bg-orange-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.tipWithTime')}
</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 > 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>{t('labUi.filmerForm.profileTitle')}</CardTitle>
<CardDescription className="text-gray-400">{t('labUi.filmerForm.profileDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>{t('labUi.filmerForm.identity')}</Label>
<div className="p-4 rounded-lg border-2 border-red-500/30 bg-red-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.filmerForm.headlineExample') })}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="bio">{t('labUi.become.bioWorkLabel')}</Label>
<Textarea id="bio" placeholder={t('labUi.filmerForm.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>
<EntityTypeField
entityType={entityType}
onEntityTypeChange={setEntityType}
members={members}
onMembersChange={setMembers}
accentBg="bg-blue-600 hover:bg-blue-700"
accentText="text-blue-400"
/>
{entityType === 'collective' && (
<CrewLinkField value={crewRef} onChange={setCrewRef} accentBg="bg-blue-600 hover:bg-blue-700" accentText="text-blue-400" />
)}
<div className="space-y-3">
<Label>{t('labUi.filmerForm.servicesLabel')}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{SERVICE_ENTRIES.map(([service, label]) => (
<button
key={service}
type="button"
onClick={() => toggleService(service)}
className={cn(
'p-3 rounded-lg text-left transition-all border text-sm',
selectedServices.includes(service)
? 'bg-red-500 text-white border-red-500'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700'
)}
>
<div className="font-bold">{FILMING_SERVICE_KEYS[service] ? t(FILMING_SERVICE_KEYS[service]) : label}</div>
<div className={cn('text-xs mt-1 line-clamp-1', selectedServices.includes(service) ? 'text-white/80' : 'text-gray-400')}>
{FILMING_SERVICE_DESC_KEYS[service] ? t(FILMING_SERVICE_DESC_KEYS[service]) : FILMING_SERVICE_DESCRIPTIONS[service]}
</div>
</button>
))}
</div>
</div>
<div className="space-y-3">
<Label>{t('labUi.filmerForm.camerasLabel')}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{SKILL_ENTRIES.map(([skill, label]) => (
<button
key={skill}
type="button"
onClick={() => toggleSkill(skill)}
className={cn(
'p-3 rounded-lg text-left transition-all border text-sm',
selectedSkills.includes(skill)
? 'bg-purple-500 text-white border-purple-500'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700'
)}
>
<div className="font-bold">{FILMING_SKILL_KEYS[skill] ? t(FILMING_SKILL_KEYS[skill]) : label}</div>
<div className={cn('text-xs mt-1 line-clamp-1', selectedSkills.includes(skill) ? 'text-white/80' : 'text-gray-400')}>
{FILMING_SKILL_DESC_KEYS[skill] ? t(FILMING_SKILL_DESC_KEYS[skill]) : FILMING_SKILL_DESCRIPTIONS[skill]}
</div>
</button>
))}
</div>
</div>
{/* Freeform gear list */}
<div className="space-y-2">
<Label htmlFor="gear">{t('labUi.filmerForm.gearLabel')}</Label>
<Textarea
id="gear"
placeholder={t('labUi.filmerForm.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.filmerForm.gearHelper')}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="experience">{t('labUi.become.yearsExperience')}</Label>
<Input id="experience" type="number" placeholder="5" 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.filmerForm.rate')}</Label>
<Input id="rate" placeholder={t('labUi.filmerForm.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="location">{t('labUi.become.cityArea')}</Label>
<Input id="location" placeholder="Los Angeles, CA" value={location} onChange={(e) => setLocation(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-3">
<Label className="text-base font-bold">{t('labUi.become.contactInfo')}</Label>
<p className="text-sm text-gray-400">{t('labUi.become.contactInfoDesc')}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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="filmer@example.com" value={email} onChange={(e) => setEmail(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="portfolio">{t('labUi.filmerForm.reelLabel')}</Label>
<Input id="portfolio" type="url" placeholder="https://vimeo.com/you" 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://mysite.com" value={website} onChange={(e) => setWebsite(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.socialMedia')}</Label>
<Input id="social" placeholder="@username" 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-blue-500/30 bg-blue-500/5">
<p className="text-sm text-gray-300">
{t('labUi.filmerForm.dmNote')}
</p>
</div>
</div>
{/* Spot Picker */}
<SpotPicker
selectedSpots={selectedSpots}
onSpotsChange={setSelectedSpots}
maxSpots={5}
label={t('labUi.filmerForm.spotsLabel')}
description={t('labUi.filmerForm.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.become.profilePhoto')}</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="block 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.turnaround')}</Label>
<Input value={turnaround} onChange={(e) => setTurnaround(e.target.value)} placeholder="e.g. 1–2 weeks" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.deliverables')}</Label>
<Input value={deliverables} onChange={(e) => setDeliverables(e.target.value)} placeholder="e.g. master + raw, 4K" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.drone')}</Label>
<Input value={drone} onChange={(e) => setDrone(e.target.value)} placeholder="e.g. FPV, Mavic 3, none" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}
accentText="text-red-400"
accentChip="bg-red-500 text-white border-red-500"
accentChipHover="hover:border-red-500/50 hover:text-red-400"
rateUnits={pickRateUnits('project', 'day', 'hour')}
/>
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" entityCoord={user ? `${SKATE_FILMER_KIND}:${user.pubkey}:filming-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-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')}
</>
) : (
<>
<Video className="h-4 w-4 mr-2" />
{existingProfile ? t('labUi.filmerForm.update') : isPro ? t('labUi.filmerForm.createPro') : t('labUi.filmerForm.create')}
</>
)}
</Button>
</CardContent>
</Card>
</form>
</div>
<LabProfilePreview
open={previewOpen}
onOpenChange={setPreviewOpen}
heading={t('labUi.detail.noun.filmer')}
noun={t('labUi.detail.noun.filmer')}
accentText="text-red-400"
accentChip="bg-red-500 text-white border-red-500"
icon={Video}
draft={{
title: titleInput.trim() || getDisplayName(),
bio, image, location, experience, rate,
extraTags,
contact: { phone, email, website, portfolio, social, noffer },
}}
/>
</>
);
}