import { useEffect, useState } from 'react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; import { PhotoDropzone } from '@/components/PhotoDropzone'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { useTranslation } from '@/contexts/language'; import { normalizeUrlInput } from '@/lib/sanitizeUrl'; import { cn } from '@/lib/utils'; import { Check, X } from 'lucide-react'; import { ALL_RATE_UNITS, AVAILABILITY_OPTIONS, DEFAULT_RATE_UNITS, PAYMENT_METHODS, RATE_CURRENCIES, type LabOption, } from '@/lib/labExtras'; interface LabExtrasFieldsProps { /** Raw tags of the existing profile, for prefill on edit. */ existingTags?: string[][]; /** * Called with the COMPLETE set of tags this component manages (after every * change, including the initial prefill), so the host form can append them on * publish without owning any of this state. */ onChange: (tags: string[][]) => void; accentText: string; accentChip: string; accentChipHover: string; /** Billing units relevant to this craft (defaults to a generic set). */ rateUnits?: LabOption[]; /** Hide the portfolio gallery (default false). */ hideGallery?: boolean; /** Hide the highlight-reel video field (default false). */ hideVideo?: boolean; } /** Tag values this component always owns, so prefill can clear stale ones. */ function Chips({ options, selected, onToggle, accentChip, accentChipHover, t, }: { options: LabOption[]; selected: string[]; onToggle: (value: string) => void; accentChip: string; accentChipHover: string; t: (key: string) => string; }) { return (
{options.map((o) => { const active = selected.includes(o.value); return ( ); })}
); } /** * The shared, craft-agnostic enrichment block for every Lab provider form: * languages, availability, structured rate, payments (+ PWYC + booking fee), * portfolio gallery, highlight reel and a booking link. * * Self-contained: it prefills from `existingTags`, owns its state, and reports * the full managed tag set via `onChange` so host forms stay tiny. */ export function LabExtrasFields({ existingTags, onChange, accentText, accentChip, accentChipHover, rateUnits = DEFAULT_RATE_UNITS, hideGallery = false, hideVideo = false, }: LabExtrasFieldsProps) { const { t } = useTranslation(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); const [languages, setLanguages] = useState([]); const [languageInput, setLanguageInput] = useState(''); const [availability, setAvailability] = useState([]); const [rateAmount, setRateAmount] = useState(''); const [rateCurrency, setRateCurrency] = useState('sats'); const [rateUnit, setRateUnit] = useState(rateUnits[0]?.value ?? 'hour'); const [paymentMethods, setPaymentMethods] = useState([]); const [paymentNote, setPaymentNote] = useState(''); const [bookingFee, setBookingFee] = useState(''); const [bookingFeeCurrency, setBookingFeeCurrency] = useState('sats'); const [payWhatYouCan, setPayWhatYouCan] = useState(false); const [photos, setPhotos] = useState([]); const [video, setVideo] = useState(''); const [booking, setBooking] = useState(''); const [prefilled, setPrefilled] = useState(false); // Prefill once, when the (async) existing profile arrives. useEffect(() => { if (prefilled || !existingTags) return; setPrefilled(true); const tag = (n: string) => existingTags.find(([t]) => t === n)?.[1]; const vals = (n: string, lower = true) => existingTags.filter(([t]) => t === n).map(([, v]) => (lower ? v.toLowerCase() : v)); setLanguages(vals('language', false)); setAvailability(vals('availability')); setRateAmount(tag('rate_amount') ?? ''); setRateCurrency(tag('rate_currency') ?? 'sats'); setRateUnit(tag('rate_unit') ?? rateUnits[0]?.value ?? 'hour'); setPaymentMethods(vals('payment_method')); setPaymentNote(tag('payment_note') ?? ''); setBookingFee(tag('booking_fee') ?? ''); setBookingFeeCurrency(tag('booking_fee_currency') ?? 'sats'); setPayWhatYouCan(existingTags.some(([t, v]) => t === 'pay_what_you_can' && v === 'true')); setPhotos(vals('photo', false)); setVideo(tag('video') ?? ''); setBooking(tag('booking') ?? ''); }, [existingTags, prefilled, rateUnits]); // Report the full managed tag set whenever anything changes. useEffect(() => { const tags: string[][] = []; for (const v of languages) tags.push(['language', v]); for (const v of availability) tags.push(['availability', v]); if (rateAmount.trim()) { tags.push(['rate_amount', rateAmount.trim()]); tags.push(['rate_currency', rateCurrency]); tags.push(['rate_unit', rateUnit]); } for (const m of paymentMethods) tags.push(['payment_method', m]); if (paymentNote.trim()) tags.push(['payment_note', paymentNote.trim()]); if (bookingFee.trim()) { tags.push(['booking_fee', bookingFee.trim()]); tags.push(['booking_fee_currency', bookingFeeCurrency]); } if (payWhatYouCan) tags.push(['pay_what_you_can', 'true']); for (const p of photos) tags.push(['photo', p]); if (video.trim()) tags.push(['video', video.trim()]); if (booking.trim()) tags.push(['booking', normalizeUrlInput(booking)]); onChange(tags); }, [languages, availability, rateAmount, rateCurrency, rateUnit, paymentMethods, paymentNote, bookingFee, bookingFeeCurrency, payWhatYouCan, photos, video, booking, onChange]); const toggle = (list: string[], setList: (v: string[]) => void, value: string) => setList(list.includes(value) ? list.filter((v) => v !== value) : [...list, value]); const addLanguage = () => { const v = languageInput.trim(); if (!v) return; setLanguages((prev) => (prev.some((l) => l.toLowerCase() === v.toLowerCase()) ? prev : [...prev, v])); setLanguageInput(''); }; const handleGalleryUpload = async (files: File[]) => { const room = 12 - photos.length; if (room <= 0) return; try { const uploaded: string[] = []; for (const file of files.slice(0, room)) { const tags = await uploadFile(file); const url = tags[0]?.[1]; if (url) uploaded.push(url); } if (uploaded.length > 0) { setPhotos((prev) => [...prev, ...uploaded].slice(0, 12)); toast({ title: t('labUi.coachForm.galleryUploaded', { count: uploaded.length }) }); } } catch (error) { toast({ title: t('labUi.become.uploadFailed'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } }; return (
{/* Languages */}

{t('labUi.coachForm.languagesHint')}

setLanguageInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addLanguage(); } }} placeholder={t('labUi.coachForm.languagePlaceholder')} className="bg-white/5 border-white/10" />
{languages.length > 0 && (
{languages.map((lang) => ( {lang} ))}
)}
{/* Availability + booking link */}
toggle(availability, setAvailability, v)} accentChip={accentChip} accentChipHover={accentChipHover} t={t} />
setBooking(e.target.value)} placeholder="https://cal.com/yourname" className="mt-1.5 bg-white/5 border-white/10" />

{t('labUi.coachForm.bookingHint')}

{/* Structured rate */}
setRateAmount(e.target.value.replace(/[^0-9.]/g, ''))} className="bg-white/5 border-white/10" />

{t('labUi.craftForm.rateLeftBlankNote')}

{/* Payments */}

{t('labUi.coachForm.paymentMethodsHint')}

toggle(paymentMethods, setPaymentMethods, v)} accentChip={accentChip} accentChipHover={accentChipHover} t={t} />
setBookingFee(e.target.value.replace(/[^0-9.]/g, ''))} className="bg-white/5 border-white/10" />

{t('labUi.coachForm.bookingFeeHint')}

setPaymentNote(e.target.value)} placeholder={t('labUi.coachForm.paymentNotePlaceholder')} className="mt-1.5 bg-white/5 border-white/10" />
{/* Portfolio gallery + reel */} {(!hideGallery || !hideVideo) && (
{!hideGallery && (

{t('labUi.coachForm.galleryHint')}

{photos.length > 0 && (
{photos.map((url) => (
))}
)} {photos.length < 12 && (
)}
)} {!hideVideo && (
setVideo(e.target.value)} placeholder="https://youtube.com/watch?v=…" className="mt-1.5 bg-white/5 border-white/10" />

{t('labUi.coachForm.videoHint')}

)}
)}
); }