import { useState, useMemo, useRef, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { Layout } from '@/components/Layout'; import { Badge } from '@/components/ui/badge'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter'; import { type SortOption } from '@/components/SortDropdown'; import { useLabProfiles, type LabProfile } from '@/hooks/useLabProfiles'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { useTranslation } from '@/contexts/LanguageContext'; import { genUserName } from '@/lib/genUserName'; import { COACH_SKILL_LABELS, FILMING_SERVICE_LABELS, FILMING_SKILL_LABELS, EDITOR_SERVICE_LABELS, EDITOR_SKILL_LABELS, PHOTO_SERVICE_LABELS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS, } from '@/lib/types'; import { GraduationCap, Search, Plus, MapPin, SlidersHorizontal, Video, Scissors, Camera, HardHat, CreditCard, X, ChevronRight, Star, Clock, Users, CheckCircle2, Shield, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { ProBadge } from '@/components/ProBadge'; import { LabCrewsStrip } from '@/components/LabCrewsStrip'; import { isFeaturedActive } from '@/lib/types'; // ─── Constants ──────────────────────────────────────────────────────────────── const COACH_SKILL_ENTRIES = Object.entries(COACH_SKILL_LABELS); const FILMING_SERVICE_ENTRIES = Object.entries(FILMING_SERVICE_LABELS); const EDITOR_SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS); const EDITOR_SKILL_ENTRIES = Object.entries(EDITOR_SKILL_LABELS); const PHOTO_SERVICE_ENTRIES = Object.entries(PHOTO_SERVICE_LABELS); const PHOTO_CAMERA_ENTRIES = Object.entries(PHOTO_CAMERA_LABELS); const PHOTO_LENS_ENTRIES = Object.entries(PHOTO_LENS_LABELS); const PHOTO_LIGHTING_ENTRIES = Object.entries(PHOTO_LIGHTING_LABELS); const PHOTO_SUPPORT_ENTRIES = Object.entries(PHOTO_SUPPORT_LABELS); const PHOTO_STYLE_ENTRIES = Object.entries(PHOTO_STYLE_LABELS); const BUILDER_SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS); const BUILDER_SKILL_ENTRIES = Object.entries(BUILDER_SKILL_LABELS); const BUILDER_SCALE_ENTRIES = Object.entries(BUILDER_SCALE_LABELS); const BUILDER_ALL_LABELS: Record = { ...BUILDER_SERVICE_LABELS, ...BUILDER_SKILL_LABELS, ...BUILDER_SCALE_LABELS, }; // All photographer label maps merged — for resolving a chip label regardless of category const PHOTO_ALL_LABELS: Record = { ...PHOTO_SERVICE_LABELS, ...PHOTO_CAMERA_LABELS, ...PHOTO_LENS_LABELS, ...PHOTO_LIGHTING_LABELS, ...PHOTO_SUPPORT_LABELS, ...PHOTO_STYLE_LABELS, }; type ServiceType = 'coaching' | 'filming' | 'editing' | 'photography' | 'building'; // ─── Per-category accent colours ───────────────────────────────────────────── const ACCENT = { coaching: { active: 'bg-[#ff0040] hover:bg-[#ff0040]/90 text-white border-[#ff0040]', chip: 'bg-[#ff0040] text-white border-[#ff0040]', chipHover: 'hover:border-[#ff0040]/60 hover:text-[#ff0040]', border: 'border-[#ff0040]/40', glow: 'shadow-[#ff0040]/20', icon: 'text-[#ff0040]', badge: 'bg-[#ff0040]/15 text-[#ff0040] border-[#ff0040]/30', stripe: 'bg-[#ff0040]', badgeSkill: 'bg-red-900/40 border-red-700/50 text-red-300', }, filming: { active: 'bg-orange-500 hover:bg-orange-600 text-white border-orange-500', chip: 'bg-orange-500 text-white border-orange-500', chipHover: 'hover:border-orange-500/60 hover:text-orange-400', border: 'border-orange-500/40', glow: 'shadow-orange-500/20', icon: 'text-orange-400', badge: 'bg-orange-500/15 text-orange-400 border-orange-500/30', stripe: 'bg-orange-500', badgeSkill: 'bg-orange-900/40 border-orange-700/50 text-orange-300', }, editing: { active: 'bg-purple-600 hover:bg-purple-700 text-white border-purple-600', chip: 'bg-purple-600 text-white border-purple-600', chipHover: 'hover:border-purple-500/60 hover:text-purple-400', border: 'border-purple-500/40', glow: 'shadow-purple-500/20', icon: 'text-purple-400', badge: 'bg-purple-500/15 text-purple-400 border-purple-500/30', stripe: 'bg-purple-600', badgeSkill: 'bg-purple-900/40 border-purple-700/50 text-purple-300', }, photography: { active: 'bg-indigo-600 hover:bg-indigo-700 text-white border-indigo-600', chip: 'bg-indigo-600 text-white border-indigo-600', chipHover: 'hover:border-indigo-500/60 hover:text-indigo-400', border: 'border-indigo-500/40', glow: 'shadow-indigo-500/20', icon: 'text-indigo-400', badge: 'bg-indigo-500/15 text-indigo-400 border-indigo-500/30', stripe: 'bg-indigo-600', badgeSkill: 'bg-indigo-900/40 border-indigo-700/50 text-indigo-300', }, building: { active: 'bg-amber-500 hover:bg-amber-600 text-black border-amber-500', chip: 'bg-amber-500 text-black border-amber-500', chipHover: 'hover:border-amber-500/60 hover:text-amber-400', border: 'border-amber-500/40', glow: 'shadow-amber-500/20', icon: 'text-amber-400', badge: 'bg-amber-500/15 text-amber-400 border-amber-500/30', stripe: 'bg-amber-500', badgeSkill: 'bg-amber-900/40 border-amber-700/50 text-amber-300', }, }; // ─── FAB — offer services ───────────────────────────────────────────────────── function OfferFAB() { const [open, setOpen] = useState(false); const ref = useRef(null); // Close on outside click useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const items = [ { to: '/become-coach', label: 'Offer Coaching', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', icon: GraduationCap }, { to: '/become-filmer', label: 'Offer Filming', color: 'bg-orange-500 hover:bg-orange-600', icon: Video }, { to: '/become-photographer', label: 'Offer Photography', color: 'bg-indigo-600 hover:bg-indigo-700', icon: Camera }, { to: '/become-editor', label: 'Offer Editing', color: 'bg-purple-600 hover:bg-purple-700', icon: Scissors }, { to: '/become-builder', label: 'Offer Building', color: 'bg-amber-500 hover:bg-amber-600 text-black', icon: HardHat }, { to: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard }, ]; return (
{/* Dropdown */} {open && (
{items.map(({ to, label, color, icon: Icon }) => ( setOpen(false)} className={cn( 'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-bold uppercase tracking-wide transition-all active:scale-95', color )} > {label} ))}
)}
); } // ─── Sticky toolbar ─────────────────────────────────────────────────────────── interface ToolbarProps { serviceType: ServiceType; onServiceChange: (t: ServiceType) => void; searchQuery: string; onSearchChange: (q: string) => void; locationQuery: string; onLocationChange: (q: string) => void; activeFilterCount: number; onOpenFilters: () => void; counts: Record; totalCount: number; isLoading: boolean; // Active filter chips data selectedSkills: string[]; onRemoveSkill: (s: string) => void; sortBy: SortOption; trustFilter: TrustFilterLevel; onClearAll: () => void; serviceType: ServiceType; } function getSkillLabelForType(skill: string, serviceType: ServiceType): string { if (serviceType === 'coaching') return COACH_SKILL_LABELS[skill] || skill; if (serviceType === 'filming') return FILMING_SERVICE_LABELS[skill] || FILMING_SKILL_LABELS[skill] || skill; if (serviceType === 'photography') return PHOTO_ALL_LABELS[skill] || skill; if (serviceType === 'building') return BUILDER_ALL_LABELS[skill] || skill; return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill; } function LabToolbar({ serviceType, onServiceChange, searchQuery, onSearchChange, locationQuery, onLocationChange, activeFilterCount, onOpenFilters, counts, totalCount, isLoading, selectedSkills, onRemoveSkill, sortBy, trustFilter, onClearAll, }: ToolbarProps) { const tabs: { key: ServiceType; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ { key: 'coaching', label: 'Coaches', icon: GraduationCap }, { key: 'filming', label: 'Filmers', icon: Video }, { key: 'photography', label: 'Photo', icon: Camera }, { key: 'editing', label: 'Editors', icon: Scissors }, { key: 'building', label: 'Builders', icon: HardHat }, ]; const accent = ACCENT[serviceType]; // Build active filter chip labels const activeChips: { key: string; label: string }[] = []; selectedSkills.forEach((s) => { activeChips.push({ key: s, label: getSkillLabelForType(s, serviceType) }); }); const hasNonSkillFilters = trustFilter !== 'all' || sortBy !== 'recent'; return (
{/* Tab row — fixed 5-across grid so every category is visible without horizontal scrolling. Icon-over-label stack keeps each tab compact but tappable on small phones; a count badge floats in the corner. */}
{tabs.map(({ key, label, icon: Icon }) => { const isActive = serviceType === key; const a = ACCENT[key]; return ( ); })}
{/* Search + Location + Filter row */}
{/* Search by name */}
onSearchChange(e.target.value)} placeholder="Name, bio…" className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all" /> {searchQuery && ( )}
{/* Location quick-filter */}
onLocationChange(e.target.value)} placeholder="City…" className="w-full h-9 pl-8 pr-3 rounded-xl bg-white/5 border border-white/10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-white/30 transition-all" /> {locationQuery && ( )}
{/* Filter button */}
{/* Active filter chips row — only shown when filters are active */} {(activeChips.length > 0 || hasNonSkillFilters) && (
{/* Skill chips */} {activeChips.map(({ key, label }) => ( ))} {/* Sort chip */} {sortBy !== 'recent' && ( Top Rated )} {/* Trust chip */} {trustFilter !== 'all' && ( {trustFilter === 'verified' ? 'Verified' : trustFilter === 'trusted' ? 'Trusted' : 'Following'} )} {/* Clear all */}
)} {/* Results count */} {!isLoading && (

{totalCount === 0 ? 'No results' : `${totalCount} result${totalCount !== 1 ? 's' : ''}`}

)}
); } // ─── Filter drawer — bottom sheet on mobile, right panel on desktop ─────────── interface FilterDrawerProps { open: boolean; onClose: () => void; serviceType: ServiceType; selectedSkills: string[]; onToggleSkill: (s: string) => void; sortBy: SortOption; onSortChange: (s: SortOption) => void; trustFilter: TrustFilterLevel; onTrustChange: (t: TrustFilterLevel) => void; onClearAll: () => void; activeCount: number; } function FilterDrawer({ open, onClose, serviceType, selectedSkills, onToggleSkill, sortBy, onSortChange, trustFilter, onTrustChange, onClearAll, activeCount, }: FilterDrawerProps) { const accent = ACCENT[serviceType]; const skillSections: { title: string; entries: [string, string][]; activeColor: string }[] = serviceType === 'coaching' ? [{ title: 'Skills to Learn', entries: COACH_SKILL_ENTRIES, activeColor: accent.chip }] : serviceType === 'filming' ? [ { title: 'Content Type', entries: FILMING_SERVICE_ENTRIES, activeColor: accent.chip }, { title: 'Equipment & Style', entries: Object.entries(FILMING_SKILL_LABELS), activeColor: 'bg-orange-400 text-white border-orange-400' }, ] : serviceType === 'photography' ? [ { title: 'Services', entries: PHOTO_SERVICE_ENTRIES, activeColor: accent.chip }, { title: 'Camera', entries: PHOTO_CAMERA_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' }, { title: 'Lenses', entries: PHOTO_LENS_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' }, { title: 'Lighting', entries: PHOTO_LIGHTING_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' }, { title: 'Support & Rig', entries: PHOTO_SUPPORT_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' }, { title: 'Style', entries: PHOTO_STYLE_ENTRIES, activeColor: 'bg-indigo-400 text-white border-indigo-400' }, ] : serviceType === 'building' ? [ { title: 'What They Build', entries: BUILDER_SERVICE_ENTRIES, activeColor: accent.chip }, { title: 'Materials & Trade', entries: BUILDER_SKILL_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' }, { title: 'Project Scale', entries: BUILDER_SCALE_ENTRIES, activeColor: 'bg-amber-400 text-black border-amber-400' }, ] : [ { title: 'Edit Type', entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip }, { title: 'Software & Tools', entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' }, ]; const trustOptions: { value: TrustFilterLevel; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ { value: 'all', label: 'All', icon: Users }, { value: 'verified', label: 'Verified', icon: CheckCircle2 }, { value: 'trusted', label: 'Trusted', icon: Shield }, { value: 'following', label: 'Following', icon: CheckCircle2 }, ]; const drawerContent = ( <> {/* Header */}
Filters {activeCount > 0 && ( {activeCount} active )}
{activeCount > 0 && ( )}
{/* Scrollable body */}
{/* Sort */}

Sort By

{[ { value: 'recent' as SortOption, label: 'Newest', icon: Clock }, { value: 'trust' as SortOption, label: 'Top Rated', icon: Star }, ].map(({ value, label, icon: Icon }) => ( ))}
{/* Trust — inline pill buttons, no dropdown */}

Trust Level

{trustOptions.map(({ value, label, icon: Icon }) => ( ))}
{/* Skill chips per section */} {skillSections.map(({ title, entries, activeColor }) => (

{title}

{entries.map(([key, label]) => { const isActive = selectedSkills.includes(key); return ( ); })}
))}
{/* Apply */}
); return ( <> {/* Mobile: bottom sheet */}
!v && onClose()}> Filters {drawerContent}
{/* Desktop: right side panel */}
!v && onClose()}> Filters {drawerContent}
); } // ─── Profile Card ───────────────────────────────────────────────────────────── function ProfileCard({ profile }: { profile: LabProfile }) { const author = useAuthor(profile.pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(profile.pubkey); const avatar = profile.image || metadata?.picture; const nip05 = metadata?.nip05; const isCoach = profile.profileType === 'coach'; const isFilmer = profile.profileType === 'filmer'; const isEditor = profile.profileType === 'editor'; const isPhotographer = profile.profileType === 'photographer'; const isBuilder = profile.profileType === 'builder'; const serviceType: ServiceType = isCoach ? 'coaching' : isFilmer ? 'filming' : isPhotographer ? 'photography' : isBuilder ? 'building' : 'editing'; const accent = ACCENT[serviceType]; const TypeIcon = isCoach ? GraduationCap : isFilmer ? Video : isPhotographer ? Camera : isBuilder ? HardHat : Scissors; const getSkillLabel = (skill: string) => { if (isCoach) return COACH_SKILL_LABELS[skill] || skill; if (isFilmer) return FILMING_SERVICE_LABELS[skill] || FILMING_SKILL_LABELS[skill] || skill; if (isPhotographer) return PHOTO_ALL_LABELS[skill] || skill; if (isBuilder) return BUILDER_ALL_LABELS[skill] || skill; if (isEditor) return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill; return skill; }; return (
{/* Left color stripe */}
{/* Avatar */}
{displayName.slice(0, 2).toUpperCase()} {/* Type badge on avatar */}
{/* Main content */}
{/* Name row */}

{displayName} {profile.entityType === 'collective' && ( {profile.members.length > 0 ? profile.members.length : ''} )}

{nip05 && (

✓ {nip05}

)}
{/* Title + location */}

{profile.title}

{profile.location && ( {profile.location} )} {profile.experience && ( {profile.experience}yr exp )}
{/* Skill chips */} {profile.skills.length > 0 && (
{profile.skills.slice(0, 3).map((skill: string) => ( {getSkillLabel(skill)} ))} {profile.skills.length > 3 && ( +{profile.skills.length - 3} )}
)} {/* Rate */} {profile.rate && (

💰 {profile.rate}

)}
); } // ─── Skeleton card ──────────────────────────────────────────────────────────── function SkeletonCard() { return (
); } // ─── Empty state per-category ───────────────────────────────────────────────── function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boolean }) { const configs = { coaching: { icon: GraduationCap, title: 'No coaches yet', body: 'Be the first to offer skateboarding coaching in your area.', cta: 'Offer Coaching', to: '/become-coach', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', }, filming: { icon: Video, title: 'No filmers yet', body: 'Be the first to offer skate filming — clips, full parts, contest coverage and more.', cta: 'Offer Filming', to: '/become-filmer', color: 'bg-orange-500 hover:bg-orange-600', }, photography: { icon: Camera, title: 'No photographers yet', body: 'Be the first to offer skate photography in your area — action shots, sequences, editorial and more.', cta: 'Offer Photography', to: '/become-photographer', color: 'bg-indigo-600 hover:bg-indigo-700', }, editing: { icon: Scissors, title: 'No video editors yet', body: 'Be the first to offer skate video editing services.', cta: 'Offer Editing', to: '/become-editor', color: 'bg-purple-600 hover:bg-purple-700', }, building: { icon: HardHat, title: 'No builders yet', body: 'Be the first to list your ramp, park or DIY building — concrete, wood, rails and more.', cta: 'Offer Building', to: '/become-builder', color: 'bg-amber-500 hover:bg-amber-600 text-black', }, }; const c = configs[serviceType]; const Icon = c.icon; return (

{c.title}

{c.body}

{user ? ( ) : (

Log in to create a profile

)} Have questions? Check the FAQ →
); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function LabPage() { const { t } = useTranslation(); useSeoMeta({ title: 'The Lab - SkateSpots', description: 'Find skateboarding coaches, filmers, photographers, and video editors.', }); const { user } = useCurrentUser(); const { data: profiles, isLoading } = useLabProfiles(100); const [searchQuery, setSearchQuery] = useState(''); const [locationQuery, setLocationQuery] = useState(''); const [selectedSkills, setSelectedSkills] = useState([]); const [serviceType, setServiceType] = useState('coaching'); const [trustFilter, setTrustFilter] = useState('all'); const [sortBy, setSortBy] = useState('recent'); const [filtersOpen, setFiltersOpen] = useState(false); const profilePubkeys = useMemo(() => profiles?.map((p) => p.pubkey) || [], [profiles]); const { data: trustScores } = useBatchTrustScores(profilePubkeys); const getTrustScore = (pubkey: string) => trustScores?.get(pubkey); // Fetch all authors for search-by-name support // We collect metadata from ProfileCard's useAuthor calls via a shared memo approach // The search includes displayName by filtering against the author metadata in ProfileCard // Since we can't call hooks in loops at the page level, we implement name search // by passing the searchQuery down and letting ProfileCard self-filter via a parent store. // Simpler: we filter by profile fields, and the rendered cards handle their own display. // Name search: we add it as a secondary match against profile.title (already done) but // also expose a `displayName` field via a lookup map we build from the query cache. // For now, profile.title is the coach's self-described role (e.g. "Skate Coach Berlin") // and the display name comes from kind 0. We add the genUserName fallback to search too. const displayNameMap = useMemo(() => { // Build a map of pubkey -> genUserName fallback for client-side name search const map = new Map(); profiles?.forEach((p) => { map.set(p.pubkey, genUserName(p.pubkey)); }); return map; }, [profiles]); const toggleSkill = (skill: string) => { setSelectedSkills((prev) => prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill] ); }; const removeSkill = (skill: string) => { setSelectedSkills((prev) => prev.filter((s) => s !== skill)); }; // Counts per tab (unfiltered by skills/search/location, just by type) const counts = useMemo>(() => { if (!profiles) return { coaching: 0, filming: 0, editing: 0, photography: 0, building: 0 }; return { coaching: profiles.filter((p) => p.profileType === 'coach').length, filming: profiles.filter((p) => p.profileType === 'filmer').length, photography: profiles.filter((p) => p.profileType === 'photographer').length, editing: profiles.filter((p) => p.profileType === 'editor').length, building: profiles.filter((p) => p.profileType === 'builder').length, }; }, [profiles]); const filteredProfiles = useMemo(() => { if (!profiles) return []; let filtered = profiles.filter((profile) => { if (serviceType === 'coaching' && profile.profileType !== 'coach') return false; if (serviceType === 'filming' && profile.profileType !== 'filmer') return false; if (serviceType === 'photography' && profile.profileType !== 'photographer') return false; if (serviceType === 'editing' && profile.profileType !== 'editor') return false; if (serviceType === 'building' && profile.profileType !== 'builder') return false; // Text search: title, bio, and genUserName fallback if (searchQuery) { const q = searchQuery.toLowerCase(); const fallbackName = displayNameMap.get(profile.pubkey) ?? ''; const matchesTitle = profile.title.toLowerCase().includes(q); const matchesBio = profile.bio?.toLowerCase().includes(q); const matchesName = fallbackName.toLowerCase().includes(q); if (!matchesTitle && !matchesBio && !matchesName) return false; } // Location filter (dedicated city search) if (locationQuery) { const lq = locationQuery.toLowerCase(); const matchesLocation = profile.location?.toLowerCase().includes(lq); if (!matchesLocation) return false; } if (selectedSkills.length > 0) { if (!profile.skills.some((s) => selectedSkills.includes(s))) return false; } return true; }); if (trustScores && trustFilter !== 'all') { filtered = filterByTrustLevel(filtered, trustFilter, getTrustScore); } filtered.sort((a, b) => { if (sortBy === 'trust') { const diff = (getTrustScore(b.pubkey) || 0) - (getTrustScore(a.pubkey) || 0); if (diff !== 0) return diff; } return b.createdAt - a.createdAt; }); return filtered; }, [profiles, searchQuery, locationQuery, selectedSkills, serviceType, trustFilter, trustScores, sortBy, displayNameMap]); // Active filter count (skills + trust + sort) — location and search are separate quick filters const activeFilterCount = selectedSkills.length + (trustFilter !== 'all' ? 1 : 0) + (sortBy !== 'recent' ? 1 : 0); const handleServiceChange = (type: ServiceType) => { setServiceType(type); setSelectedSkills([]); }; const handleClearAll = () => { setSelectedSkills([]); setTrustFilter('all'); setSortBy('recent'); }; return (
{/* ── Page header ── */}

THE{' '} LAB

{t('labPage.subtitle')}

{/* CTA — logged in: FAB dropdown. Logged out: small badge */} {user ? ( ) : ( 🔒 Login to offer services )}
{/* ── Sticky toolbar (tabs + search + location + filter button + active chips) ── */} setFiltersOpen(true)} counts={counts} totalCount={filteredProfiles.length} isLoading={isLoading} selectedSkills={selectedSkills} onRemoveSkill={removeSkill} sortBy={sortBy} trustFilter={trustFilter} onClearAll={handleClearAll} /> {/* ── Results grid ── */}
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => )}
) : filteredProfiles.length > 0 ? (
{filteredProfiles.map((profile) => ( ))}
) : ( )}
{/* ── Crews in this scene ── */}
{/* ── Filter drawer ── */} setFiltersOpen(false)} serviceType={serviceType} selectedSkills={selectedSkills} onToggleSkill={toggleSkill} sortBy={sortBy} onSortChange={setSortBy} trustFilter={trustFilter} onTrustChange={setTrustFilter} onClearAll={handleClearAll} activeCount={activeFilterCount} />
); }