import { dateLocaleCode } from '@/lib/dateFnsI18n'; import { useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'; import { PageError } from '@/components/ErrorState'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { ZapButton } from '@/components/ZapButton'; import { ClinkPayButton } from '@/components/ClinkPayButton'; import { CollectiveMembers } from '@/components/CollectiveMembers'; import { RequestToJoinCollective } from '@/components/RequestToJoinCollective'; import { ConfirmMembershipCard } from '@/components/ConfirmMembershipCard'; import { VouchCard } from '@/components/VouchCard'; import { LinkedCrewCard } from '@/components/CrossLinkCards'; import { TrustBadge } from '@/components/TrustBadge'; import { TrustFlaggedNotice } from '@/components/TrustFlaggedNotice'; import { useLabProfile } from '@/hooks/useLabProfiles'; import { useGigs } from '@/hooks/useGigs'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { isAdminPubkey } from '@/lib/admins'; import { COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, PHOTO_SERVICE_LABELS, PHOTO_SERVICE_DESCRIPTIONS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SERVICE_DESCRIPTIONS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS, ARTIST_SERVICE_LABELS, ARTIST_SERVICE_DESCRIPTIONS, ARTIST_MEDIUM_LABELS, ARTIST_TOOL_LABELS, GRAFFITI_STYLE_LABELS, GRAFFITI_STYLE_DESCRIPTIONS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS, TATTOO_STYLE_LABELS, TATTOO_STYLE_DESCRIPTIONS, TATTOO_METHOD_LABELS, TRAINER_FOCUS_LABELS, TRAINER_FOCUS_DESCRIPTIONS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS, SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, SKATE_BUILDER_KIND, SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_KIND, ORGANIZER_EVENT_LABELS, ORGANIZER_SERVICE_LABELS, ORGANIZER_SCALE_LABELS, BARBER_SERVICE_LABELS, BARBER_SERVICE_DESCRIPTIONS, BARBER_STYLE_LABELS, BARBER_SETTING_LABELS, SKATE_BARBER_KIND, SKATE_TRANSPORT_KIND, TRANSPORT_VEHICLE_LABELS, TRANSPORT_SERVICE_LABELS, TRANSPORT_FEATURE_LABELS } from '@/lib/types'; import { COACH_SKILL_KEYS, COACH_SKILL_DESC_KEYS, FILMING_SERVICE_KEYS, FILMING_SERVICE_DESC_KEYS, FILMING_SKILL_KEYS, FILMING_SKILL_DESC_KEYS, EDITOR_SERVICE_KEYS, EDITOR_SERVICE_DESC_KEYS, EDITOR_SKILL_KEYS, EDITOR_SKILL_DESC_KEYS, PHOTO_SERVICE_KEYS, PHOTO_SERVICE_DESC_KEYS, PHOTO_CAMERA_KEYS, PHOTO_LENS_KEYS, PHOTO_LIGHTING_KEYS, PHOTO_SUPPORT_KEYS, PHOTO_STYLE_KEYS, BUILDER_SERVICE_KEYS, BUILDER_SERVICE_DESC_KEYS, BUILDER_SKILL_KEYS, BUILDER_SCALE_KEYS, ARTIST_SERVICE_KEYS, ARTIST_SERVICE_DESC_KEYS, ARTIST_MEDIUM_KEYS, ARTIST_TOOL_KEYS, GRAFFITI_STYLE_KEYS, GRAFFITI_STYLE_DESC_KEYS, GRAFFITI_MEDIUM_KEYS, GRAFFITI_CONTEXT_KEYS, TATTOO_STYLE_KEYS, TATTOO_STYLE_DESC_KEYS, TATTOO_METHOD_KEYS, TRAINER_FOCUS_KEYS, TRAINER_FOCUS_DESC_KEYS, TRAINER_INJURY_KEYS, TRAINER_CREDENTIAL_KEYS, MUSICIAN_GENRE_KEYS, MUSICIAN_INSTRUMENT_KEYS, JUDGE_FORMAT_KEYS, JUDGE_SPECIALTY_KEYS, JUDGE_LEVEL_KEYS, ORGANIZER_EVENT_KEYS, ORGANIZER_SERVICE_KEYS, ORGANIZER_SCALE_KEYS, BARBER_SERVICE_KEYS, BARBER_SERVICE_DESC_KEYS, BARBER_STYLE_KEYS, BARBER_SETTING_KEYS, TRANSPORT_VEHICLE_KEYS, TRANSPORT_SERVICE_KEYS, TRANSPORT_FEATURE_KEYS, } from '@/lib/labelKeys'; import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coachOptionKey, coachRateCurrencyKey, coachRateUnitKey, type CoachOption } from '@/lib/coachOptions'; import { parseLabExtras } from '@/lib/labExtras'; import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields'; import { labProfilePath } from '@/lib/labRoutes'; import { LabExtrasSection } from '@/components/LabExtrasSection'; import { ShareProfileDialog } from '@/components/ShareProfileDialog'; import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl'; import { useTranslation } from '@/contexts/language'; import { OfficialBadge } from '@/components/OfficialBadge'; import { ProBadge } from '@/components/ProBadge'; import NotFound from './NotFound'; import { GraduationCap, MapPin, ArrowLeft, MessageCircle, Mail, Phone, Globe, Instagram, Calendar, Video, Camera, Scissors, Aperture, Users, HardHat, Palette, SprayCan, Brush, Dumbbell, Music, Gavel, Tent, Sparkles, Truck, ShieldCheck, ShieldAlert, Share2, Pencil, PartyPopper, X, } from 'lucide-react'; import { nip19 } from 'nostr-tools'; function CoachDetailSkeleton() { return ( <>
); } export default function CoachDetailPage() { const { t } = useTranslation(); const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const { data: profile, isLoading, error, refetch } = useLabProfile(identifier || '', pubkey); const author = useAuthor(profile?.pubkey); const { user } = useCurrentUser(); const navigate = useNavigate(); const [shareOpen, setShareOpen] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const justPublished = searchParams.get('published') === '1'; // Gigs — only relevant for musicians. The hook is disabled for other types. const { data: gigs = [] } = useGigs( profile?.profileType === 'musician' ? profile.pubkey : undefined, profile?.profileType === 'musician' ? profile.identifier : undefined, ); const isCoach = profile?.profileType === 'coach'; const isFilmer = profile?.profileType === 'filmer'; const isPhotographer = profile?.profileType === 'photographer'; const isBuilder = profile?.profileType === 'builder'; const isGraffiti = profile?.profileType === 'graffiti'; const isTattoo = profile?.profileType === 'tattoo'; const isTrainer = profile?.profileType === 'trainer'; const isMusician = profile?.profileType === 'musician'; const isEditor = profile?.profileType === 'editor'; const isJudge = profile?.profileType === 'judge'; const isOrganizer = profile?.profileType === 'organizer'; const isBarber = profile?.profileType === 'barber'; useSeoMeta({ title: profile ? `${profile.title} - SkateSpots` : t('legacyMiscUi.seoProfileTitle'), description: profile?.bio || t('legacyMiscUi.seoViewProfile'), }); const metadata = author.data?.metadata; if (isLoading) { return ; } if (error) { return refetch()} />; } if (!profile) { return ; } const npub = nip19.npubEncode(profile.pubkey); // Per-type accent (text, border, bg, ring) — keeps the detail page on-brand const ACCENTS: Record = { coach: { text: 'text-[#ff0040]', bg: 'bg-[#ff0040]', border: 'border-[#ff0040]/30', hover: 'hover:bg-[#ff0040]/10 border-[#ff0040]' }, filmer: { text: 'text-orange-500', bg: 'bg-orange-500', border: 'border-orange-500/30', hover: 'hover:bg-orange-500/10 border-orange-500' }, photographer: { text: 'text-indigo-400', bg: 'bg-indigo-600', border: 'border-indigo-500/30', hover: 'hover:bg-indigo-500/10 border-indigo-500' }, builder: { text: 'text-amber-400', bg: 'bg-amber-500', border: 'border-amber-500/30', hover: 'hover:bg-amber-500/10 border-amber-500' }, editor: { text: 'text-purple-400', bg: 'bg-purple-600', border: 'border-purple-500/30', hover: 'hover:bg-purple-500/10 border-purple-500' }, artist: { text: 'text-fuchsia-400', bg: 'bg-fuchsia-600', border: 'border-fuchsia-500/30', hover: 'hover:bg-fuchsia-500/10 border-fuchsia-500' }, graffiti: { text: 'text-lime-400', bg: 'bg-lime-500', border: 'border-lime-500/30', hover: 'hover:bg-lime-500/10 border-lime-500' }, tattoo: { text: 'text-rose-400', bg: 'bg-rose-600', border: 'border-rose-500/30', hover: 'hover:bg-rose-500/10 border-rose-500' }, trainer: { text: 'text-emerald-400', bg: 'bg-emerald-600', border: 'border-emerald-500/30', hover: 'hover:bg-emerald-500/10 border-emerald-500' }, musician: { text: 'text-sky-400', bg: 'bg-sky-500', border: 'border-sky-500/30', hover: 'hover:bg-sky-500/10 border-sky-500' }, judge: { text: 'text-yellow-400', bg: 'bg-yellow-500', border: 'border-yellow-500/30', hover: 'hover:bg-yellow-500/10 border-yellow-500' }, organizer: { text: 'text-teal-400', bg: 'bg-teal-600', border: 'border-teal-500/30', hover: 'hover:bg-teal-500/10 border-teal-500' }, barber: { text: 'text-cyan-400', bg: 'bg-cyan-600', border: 'border-cyan-500/30', hover: 'hover:bg-cyan-500/10 border-cyan-500' }, transport: { text: 'text-amber-400', bg: 'bg-amber-500', border: 'border-amber-500/30', hover: 'hover:bg-amber-500/10 border-amber-500' }, }; const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach; // The Lab profile is a union of craft-specific shapes. Craft-specific fields // (gear, studio, credentials, instagram, …) only exist on some members, so we // read them through a single typed accessor that returns an optional string // instead of scattering casts across the JSX. Presence is still gated by the // isCoach/isBuilder/… flags before these are shown. const pf = profile as unknown as Record; const pget = (key: string): string | undefined => { const v = pf[key]; return typeof v === 'string' && v.length > 0 ? v : undefined; }; const pbool = (key: string): boolean => pf[key] === true; /** Repeatable string-array field on the union profile (empty when absent). */ const parr = (key: string): string[] => { const v = pf[key]; return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []; }; /** Translated label for a stored option value, falling back to the raw value. */ const optLabel = (options: CoachOption[], value: string): string => { const key = coachOptionKey(options, value); return key ? t(key) : value; }; // Shared craft-agnostic extras (languages, availability, rate, payments, // gallery, reel) — read straight off the event so every Lab craft gets them. const extras = parseLabExtras(profile.event); // Structured rate (`rate_amount`/`rate_currency`/`rate_unit`) with a fallback // to the legacy freeform `rate` string. const rateAmount = extras.rateAmount; const rateCurrencyKey = coachRateCurrencyKey(extras.rateCurrency); const rateUnitKey = coachRateUnitKey(extras.rateUnit); const rateDisplay = rateAmount ? `${rateAmount} ${rateCurrencyKey ? t(rateCurrencyKey) : (extras.rateCurrency ?? '')}${rateUnitKey ? ` · ${t(rateUnitKey)}` : ''}`.trim() : pget('rate'); const TYPE_ICONS: Record = { coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors, artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel, organizer: Tent, barber: Sparkles, transport: Truck, }; const TypeIcon = TYPE_ICONS[profile.profileType] ?? GraduationCap; // Kind for the collective coordinate (for NIP-32 membership attestations) const KINDS: Record = { coach: SKATE_COACH_KIND, filmer: SKATE_FILMER_KIND, editor: SKATE_EDITOR_KIND, photographer: SKATE_PHOTOGRAPHER_KIND, builder: SKATE_BUILDER_KIND, artist: SKATE_ARTIST_KIND, graffiti: SKATE_GRAFFITI_KIND, tattoo: SKATE_TATTOO_KIND, trainer: SKATE_TRAINER_KIND, musician: SKATE_MUSICIAN_KIND, judge: SKATE_JUDGE_KIND, organizer: SKATE_ORGANIZER_KIND, barber: SKATE_BARBER_KIND, transport: SKATE_TRANSPORT_KIND, }; const profileKind = KINDS[profile.profileType] ?? SKATE_COACH_KIND; const isCollective = profile.entityType === 'collective' && profile.members.length > 0; // Merged label maps per craft for chip resolution const LABELS_BY_TYPE: Record> = { coach: COACH_SKILL_LABELS, filmer: { ...FILMING_SERVICE_LABELS, ...FILMING_SKILL_LABELS }, editor: { ...EDITOR_SERVICE_LABELS, ...EDITOR_SKILL_LABELS }, photographer: { ...PHOTO_SERVICE_LABELS, ...PHOTO_CAMERA_LABELS, ...PHOTO_LENS_LABELS, ...PHOTO_LIGHTING_LABELS, ...PHOTO_SUPPORT_LABELS, ...PHOTO_STYLE_LABELS }, builder: { ...BUILDER_SERVICE_LABELS, ...BUILDER_SKILL_LABELS, ...BUILDER_SCALE_LABELS }, artist: { ...ARTIST_SERVICE_LABELS, ...ARTIST_MEDIUM_LABELS, ...ARTIST_TOOL_LABELS }, graffiti: { ...GRAFFITI_STYLE_LABELS, ...GRAFFITI_MEDIUM_LABELS, ...GRAFFITI_CONTEXT_LABELS }, tattoo: { ...TATTOO_STYLE_LABELS, ...TATTOO_METHOD_LABELS }, trainer: { ...TRAINER_FOCUS_LABELS, ...TRAINER_INJURY_LABELS, ...TRAINER_CREDENTIAL_LABELS }, musician: { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS }, judge: { ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS }, organizer: { ...ORGANIZER_EVENT_LABELS, ...ORGANIZER_SERVICE_LABELS, ...ORGANIZER_SCALE_LABELS }, barber: { ...BARBER_SERVICE_LABELS, ...BARBER_STYLE_LABELS, ...BARBER_SETTING_LABELS }, transport: { ...TRANSPORT_VEHICLE_LABELS, ...TRANSPORT_SERVICE_LABELS, ...TRANSPORT_FEATURE_LABELS }, }; const DESCRIPTIONS_BY_TYPE: Record> = { coach: COACH_SKILL_DESCRIPTIONS, filmer: { ...FILMING_SERVICE_DESCRIPTIONS, ...FILMING_SKILL_DESCRIPTIONS }, editor: { ...EDITOR_SERVICE_DESCRIPTIONS, ...EDITOR_SKILL_DESCRIPTIONS }, photographer: PHOTO_SERVICE_DESCRIPTIONS, builder: BUILDER_SERVICE_DESCRIPTIONS, artist: ARTIST_SERVICE_DESCRIPTIONS, graffiti: GRAFFITI_STYLE_DESCRIPTIONS, tattoo: TATTOO_STYLE_DESCRIPTIONS, trainer: TRAINER_FOCUS_DESCRIPTIONS, musician: {}, judge: {}, organizer: {}, barber: BARBER_SERVICE_DESCRIPTIONS, transport: {}, }; const LABEL_KEYS_BY_TYPE: Record> = { coach: COACH_SKILL_KEYS, filmer: { ...FILMING_SERVICE_KEYS, ...FILMING_SKILL_KEYS }, editor: { ...EDITOR_SERVICE_KEYS, ...EDITOR_SKILL_KEYS }, photographer: { ...PHOTO_SERVICE_KEYS, ...PHOTO_CAMERA_KEYS, ...PHOTO_LENS_KEYS, ...PHOTO_LIGHTING_KEYS, ...PHOTO_SUPPORT_KEYS, ...PHOTO_STYLE_KEYS }, builder: { ...BUILDER_SERVICE_KEYS, ...BUILDER_SKILL_KEYS, ...BUILDER_SCALE_KEYS }, artist: { ...ARTIST_SERVICE_KEYS, ...ARTIST_MEDIUM_KEYS, ...ARTIST_TOOL_KEYS }, graffiti: { ...GRAFFITI_STYLE_KEYS, ...GRAFFITI_MEDIUM_KEYS, ...GRAFFITI_CONTEXT_KEYS }, tattoo: { ...TATTOO_STYLE_KEYS, ...TATTOO_METHOD_KEYS }, trainer: { ...TRAINER_FOCUS_KEYS, ...TRAINER_INJURY_KEYS, ...TRAINER_CREDENTIAL_KEYS }, musician: { ...MUSICIAN_GENRE_KEYS, ...MUSICIAN_INSTRUMENT_KEYS }, judge: { ...JUDGE_FORMAT_KEYS, ...JUDGE_SPECIALTY_KEYS, ...JUDGE_LEVEL_KEYS }, organizer: { ...ORGANIZER_EVENT_KEYS, ...ORGANIZER_SERVICE_KEYS, ...ORGANIZER_SCALE_KEYS }, barber: { ...BARBER_SERVICE_KEYS, ...BARBER_STYLE_KEYS, ...BARBER_SETTING_KEYS }, transport: { ...TRANSPORT_VEHICLE_KEYS, ...TRANSPORT_SERVICE_KEYS, ...TRANSPORT_FEATURE_KEYS }, }; const DESC_KEYS_BY_TYPE: Record> = { coach: COACH_SKILL_DESC_KEYS, filmer: { ...FILMING_SERVICE_DESC_KEYS, ...FILMING_SKILL_DESC_KEYS }, editor: { ...EDITOR_SERVICE_DESC_KEYS, ...EDITOR_SKILL_DESC_KEYS }, photographer: PHOTO_SERVICE_DESC_KEYS, builder: BUILDER_SERVICE_DESC_KEYS, artist: ARTIST_SERVICE_DESC_KEYS, graffiti: GRAFFITI_STYLE_DESC_KEYS, tattoo: TATTOO_STYLE_DESC_KEYS, trainer: TRAINER_FOCUS_DESC_KEYS, barber: BARBER_SERVICE_DESC_KEYS, transport: {}, }; const getSkillLabel = (skill: string) => { const key = LABEL_KEYS_BY_TYPE[profile.profileType]?.[skill]; return key ? t(key) : (LABELS_BY_TYPE[profile.profileType]?.[skill] || skill); }; const getSkillDescription = (skill: string): string | undefined => { const key = DESC_KEYS_BY_TYPE[profile.profileType]?.[skill]; return key ? t(key) : DESCRIPTIONS_BY_TYPE[profile.profileType]?.[skill]; }; const TYPE_NOUNS: Record = { coach: t('labUi.detail.noun.coach'), filmer: t('labUi.detail.noun.filmer'), editor: t('labUi.detail.noun.editor'), photographer: t('labUi.detail.noun.photographer'), builder: t('labUi.detail.noun.builder'), artist: t('labUi.detail.noun.artist'), graffiti: t('labUi.detail.noun.graffiti'), tattoo: t('labUi.detail.noun.tattoo'), trainer: t('labUi.detail.noun.trainer'), musician: t('labUi.detail.noun.musician'), judge: t('labUi.detail.noun.judge'), organizer: t('labUi.detail.noun.organizer'), barber: t('labUi.detail.noun.barber'), transport: t('labUi.detail.noun.transport'), }; const typeNoun = TYPE_NOUNS[profile.profileType] ?? t('labUi.detail.noun.profile'); const SKILLS_TITLES: Record = { coach: t('labUi.detail.skillsTitle.skills'), photographer: t('labUi.detail.skillsTitle.servicesGear'), builder: t('labUi.detail.skillsTitle.whatWeBuild'), artist: t('labUi.detail.skillsTitle.whatTheyMake'), graffiti: t('labUi.detail.skillsTitle.stylesWork'), tattoo: t('labUi.detail.skillsTitle.styles'), trainer: t('labUi.detail.skillsTitle.focusSpecialties'), musician: t('labUi.detail.skillsTitle.genresInstruments'), judge: t('labUi.detail.skillsTitle.formatsSpecialties'), barber: t('labUi.detail.skillsTitle.servicesStyles'), organizer: t('labUi.detail.skillsTitle.eventTypesServices'), transport: t('labUi.detail.skillsTitle.transport'), }; const skillsTitle = SKILLS_TITLES[profile.profileType] ?? t('labUi.detail.servicesSkills'); return ( <>
{t('labUi.become.backToLab')} {profile.profileType === 'transport' && (

{t('labUi.detail.transportSelfAttestedTitle')}

{t('labUi.detail.transportSelfAttested')}

)} {justPublished && user?.pubkey === profile.pubkey && (

{t('labUi.detail.publishedTitle')}

{t('labUi.detail.publishedBody')}

)}
{/* Main Content */}
{/* Confirm-membership prompt (only for listed pending members) */} {isCollective && ( )} {/* Profile Card */}
{profile.title.slice(0, 2).toUpperCase()}

{profile.title}

{profile.entityType === 'collective' && ( {t('labUi.collective')} )} {user?.pubkey === profile.pubkey && ( )}

{t('labUi.detail.joined')} {new Date(profile.createdAt * 1000).toLocaleDateString(dateLocaleCode())}

{/* Bio */} {profile.bio && (

{t('labUi.detail.about')}

{profile.bio}

)} {/* Skills */} {profile.skills.length > 0 && (

{skillsTitle}

{profile.skills.map((skill) => (
{getSkillLabel(skill)} {getSkillDescription(skill) && (

{getSkillDescription(skill)}

)}
))}
)} {/* Coach — credentials, formats, levels, ages, languages, availability */} {isCoach && (parr('credentials').length > 0 || pget('certNote') || parr('formats').length > 0 || parr('levels').length > 0 || parr('ageGroups').length > 0) && (
{(parr('credentials').length > 0 || pget('certNote')) && (

{t('labUi.detail.credentialsTitle')}

{parr('credentials').length > 0 && (
{parr('credentials').map((c) => ( {optLabel(COACH_CREDENTIALS, c)} ))}
)} {pget('certNote') && (

{pget('certNote')}

)}

{t('labUi.detail.selfAttested')}

)} {(parr('formats').length > 0 || parr('levels').length > 0 || parr('ageGroups').length > 0) && (
{parr('formats').length > 0 && (

{t('labUi.detail.formats')}

{parr('formats').map((v) => ( {optLabel(COACH_FORMATS, v)} ))}
)} {parr('levels').length > 0 && (

{t('labUi.detail.levels')}

{parr('levels').map((v) => ( {optLabel(COACH_LEVELS, v)} ))}
)} {parr('ageGroups').length > 0 && (

{t('labUi.detail.ages')}

{parr('ageGroups').map((v) => ( {optLabel(COACH_AGE_GROUPS, v)} ))}
)}
)}
)} {/* Shared extras (all crafts): payments, languages, availability, portfolio */} {/* Craft-specific freeform details (see lib/craftFields) */} {(() => { const fields = (CRAFT_DETAIL_FIELDS[profile.profileType] ?? []).filter((f) => pget(f.tag)); if (fields.length === 0) return null; return (
{fields.map((f) => (

{t(f.labelKey)}

{pget(f.tag) === 'true' ? t('common.yes') : pget(f.tag)}

))}
); })()} {/* In the Bag — photographer / filmer gear (freeform) */} {(isPhotographer || isFilmer || isBuilder) && pget('gear') && (

{isBuilder ? : } {isBuilder ? t('labUi.detail.howWeWork') : t('labUi.detail.inTheBag')}

{pget('gear')}

)} {/* Editor — hardware & software */} {isEditor && (pget('hardware') || pget('software')) && (
{pget('hardware') && (

{t('labUi.detail.hardware')}

{pget('hardware')}

)} {pget('software') && (

{t('labUi.detail.software')}

{pget('software')}

)}
)} {/* Judge — events judged */} {isJudge && pget('events_judged') && (

{t('labUi.detail.eventsJudged')}

{pget('events_judged')}

)} {/* Organizer — events organized */} {isOrganizer && pget('events_hosted') && (

{t('labUi.detail.eventsOrganized')}

{pget('events_hosted')}

)} {/* Spots — places this provider works / skates */} {'spotRefs' in profile && profile.spotRefs && profile.spotRefs.length > 0 && (

{t('labUi.detail.spots')}

{profile.spotRefs.map((ref) => { const [kind, pk, ...rest] = ref.split(':'); if (kind !== '30818' || !pk || rest.length === 0) return null; return ( {t('labUi.detail.viewSpot')} ); })}
)} {/* Builder — service area & insured */} {isBuilder && (pget('serviceArea') || pbool('insured')) && (
{pget('serviceArea') && (

{t('labUi.detail.serviceArea')}

{pget('serviceArea')}

)} {pbool('insured') && (
{t('labUi.detail.licensedInsured')}
)}
)} {/* Tattoo — studio & books-open */} {isTattoo && (pget('studio') || pbool('booksOpen')) && (
{pget('studio') && (

{t('labUi.detail.studio')}

{pget('studio')}

)} {pbool('booksOpen') && (
{t('labUi.detail.booksOpen')}
)}
)} {/* Barber — shop & books-open */} {isBarber && (pget('studio') || pbool('booksOpen')) && (
{pget('studio') && (

{t('labUi.detail.shopStudio')}

{pget('studio')}

)} {pbool('booksOpen') && (
{t('labUi.detail.booksOpen')}
)}
)} {/* Trainer — credentials & remote */} {isTrainer && (pget('credentials') || pbool('remote')) && (
{pget('credentials') && (

{t('labUi.detail.credentials')}

{pget('credentials')}

)} {pbool('remote') && (
{t('labUi.detail.remoteOnline')}
)}
)} {/* Graffiti — crew name */} {isGraffiti && pget('crew') && (

{t('labUi.detail.graffitiCrew')}

{pget('crew')}

)} {/* Musician — act type & listen links */} {isMusician && (
{'actType' in profile && profile.actType && (

{t('labUi.detail.act')}

{profile.actType}

)} {(('spotify' in profile && profile.spotify) || ('bandcamp' in profile && profile.bandcamp) || ('soundcloud' in profile && profile.soundcloud) || ('youtube' in profile && profile.youtube)) && (

{t('labUi.detail.listen')}

{'spotify' in profile && sanitizeUrl(profile.spotify) && ( Spotify )} {'bandcamp' in profile && sanitizeUrl(profile.bandcamp) && ( Bandcamp )} {'soundcloud' in profile && sanitizeUrl(profile.soundcloud) && ( SoundCloud )} {'youtube' in profile && sanitizeUrl(profile.youtube) && ( YouTube )}
)}
)} {/* Upcoming gigs — events that feature this musician */} {isMusician && gigs.length > 0 && (

{t('labUi.detail.upcomingGigs')}

{gigs.slice(0, 6).map((gig) => { const isUpcoming = gig.start >= Math.floor(Date.now() / 1000); return (

{gig.title}

{new Date(gig.start * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' })} {gig.location ? ` · ${gig.location}` : ''}

{!isUpcoming && ( {t('labUi.detail.past')} )} ); })}
)} {/* Experience & Rate */}
{profile.experience && (

{t('labUi.detail.experience')}

{t('labUi.detail.yearsCount', { count: profile.experience })}

)} {rateDisplay && (

{t('labUi.detail.rate')}

{rateDisplay}

{rateAmount && pget('rate') && (

{pget('rate')}

)}
)}
{/* Sidebar */}
{/* Contact Card */} {t('labUi.detail.contact')} {/* Direct Message */} {/* Book this act for an event — deep-links to event creation pre-tagged with this musician (Music → Events). */} {isMusician && ( )} {/* Contact Info from Profile */} {sanitizeTel(profile.phone) && ( {profile.phone} )} {sanitizeEmail(profile.email) && ( {profile.email} )} {sanitizeUrl(profile.website) && ( {urlDomain(profile.website!)} )} {isCoach && pget('instagram') && ( @{pget('instagram')!.replace('@', '')} )} {!isCoach && 'social' in profile && sanitizeUrl(profile.social) && ( {urlDomain(profile.social!)} )} {/* Portfolio / reel (all crafts except coach) */} {!isCoach && 'portfolio' in profile && sanitizeUrl(profile.portfolio) && ( {t('labUi.detail.portfolioDomain', { domain: urlDomain(profile.portfolio!) })} )} {/* Lightning Address from Nostr Profile */} {metadata?.lud16 && (
{metadata.lud16}
)} {/* Zap Button */}
{/* CLINK self-custodial payment (only when a valid noffer is present) */} {'noffer' in profile && profile.noffer && ( )}
{/* Scene vouches — community anti-fraud / trust signal */} {/* Owner controls — delete your own Lab profile; admins can take down any profile (DeleteEntityDialog gates owner-or-admin). */} {user && (user.pubkey === profile.pubkey || isAdminPubkey(user.pubkey)) && ( {user.pubkey === profile.pubkey ? t('labUi.detail.manage') : t('labUi.detail.admin')}

{user.pubkey === profile.pubkey ? t('labUi.detail.manageRemove', { noun: typeNoun }) : t('labUi.detail.adminTakeDown', { noun: typeNoun })}

{user.pubkey === profile.pubkey && ( )}
)} {/* Members Card (collectives only) */} {isCollective && ( )} {/* Request to join (collectives only) — shown to non-owner, non-member skaters; notifies the owner via their bell. */} {isCollective && ( )} {/* Linked crew (cross-link) — pass this profile's own coord so a mutual link shows a "Verified" badge. */} {/* Location Card */} {profile.location && ( {t('labUi.detail.location')}

{profile.location}

)}
); }