import { useState, useMemo } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link } from 'react-router-dom'; import { LazyMount } from '@/components/LazyMount'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { CrewRepBadge, CrewActivityPulse } from '@/components/CrewBadge'; import { EmptyState as SharedEmptyState } from '@/components/EmptyState'; import { useTranslation } from '@/contexts/language'; import { useCrews } from '@/hooks/useCrews'; import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity'; import { useGeolocation } from '@/hooks/useGeolocation'; import { crewAccent, crewDistanceKm, formatDistance, type LatLng } from '@/lib/crewHelpers'; import type { SkateCrew, CrewFocus } from '@/lib/types'; import { CREW_VIBE_KEYS, CREW_FOCUS_KEYS } from '@/lib/labelKeys'; import type { CrewActivity } from '@/lib/crewHelpers'; import { cn } from '@/lib/utils'; import { Users, Plus, Search, MapPin, X, HardHat, Camera, GraduationCap, Sparkles, Navigation, Loader2, Music, Palette, Dumbbell, Film, ChevronDown, Check } from 'lucide-react'; const FOCUS_BADGE_ICONS: Record = { mixed: Sparkles, builder: HardHat, photo: Camera, coaching: GraduationCap, art: Palette, music: Music, wellness: Dumbbell, }; function CrewCard({ crew, activity, distanceKm, }: { crew: SkateCrew; activity?: CrewActivity; distanceKm: number | null; }) { const { t } = useTranslation(); const accent = crewAccent(crew); const FocusIcon = crew.focus !== 'mixed' ? FOCUS_BADGE_ICONS[crew.focus] : null; return ( {crew.name.slice(0, 2).toUpperCase()}
{/* Name + member count */}

{crew.name}

{crew.members.length}
{/* Location line */} {(crew.location || distanceKm !== null) && (

{crew.location} {distanceKm !== null && ( · {formatDistance(distanceKm)} )}

)} {/* Badges — all on one wrapping row, clipped so nothing spills */}
{FocusIcon && ( {CREW_FOCUS_KEYS[crew.focus] ? t(CREW_FOCUS_KEYS[crew.focus]) : crew.focus} )} {crew.videoUrl && ( {t('socialUi.crews.part')} )} {crew.bandName && ( {t('socialUi.crews.band')} )} {crew.vibes.slice(0, 2).map((v) => ( {CREW_VIBE_KEYS[v] ? t(CREW_VIBE_KEYS[v]) : v} ))}
); } export default function CrewsPage() { const { t } = useTranslation(); useSeoMeta({ title: t('socialUi.crews.seoTitle'), description: t('socialUi.crews.seoDescription'), }); const { data: crews, isLoading } = useCrews(200); const { data: activityMap } = useCrewsActivity(crews); const geo = useGeolocation(); const [query, setQuery] = useState(''); const [focusFilter, setFocusFilter] = useState('all'); const [focusOpen, setFocusOpen] = useState(false); const [nearMe, setNearMe] = useState(false); // Remembered preference: compact short labels vs. the full filter names. const FOCUS_FILTERS: { key: CrewFocus | 'all'; label: string; short: string; icon: typeof Users }[] = [ { key: 'all', label: t('socialUi.crews.focusAll'), short: t('socialUi.crews.focusAllShort'), icon: Users }, { key: 'builder', label: CREW_FOCUS_KEYS.builder ? t(CREW_FOCUS_KEYS.builder) : 'builder', short: t('socialUi.crews.focusDiy'), icon: HardHat }, { key: 'photo', label: CREW_FOCUS_KEYS.photo ? t(CREW_FOCUS_KEYS.photo) : 'photo', short: t('socialUi.crews.focusPhoto'), icon: Camera }, { key: 'coaching', label: CREW_FOCUS_KEYS.coaching ? t(CREW_FOCUS_KEYS.coaching) : 'coaching', short: t('socialUi.crews.focusCoaching'), icon: GraduationCap }, { key: 'art', label: CREW_FOCUS_KEYS.art ? t(CREW_FOCUS_KEYS.art) : 'art', short: t('socialUi.crews.focusArt'), icon: Palette }, { key: 'music', label: CREW_FOCUS_KEYS.music ? t(CREW_FOCUS_KEYS.music) : 'music', short: t('socialUi.crews.focusMusic'), icon: Music }, { key: 'wellness', label: CREW_FOCUS_KEYS.wellness ? t(CREW_FOCUS_KEYS.wellness) : 'wellness', short: t('socialUi.crews.focusPhysio'), icon: Dumbbell }, { key: 'mixed', label: CREW_FOCUS_KEYS.mixed ? t(CREW_FOCUS_KEYS.mixed) : 'mixed', short: t('socialUi.crews.focusMixed'), icon: Sparkles }, ]; const currentFocus = FOCUS_FILTERS.find((f) => f.key === focusFilter) ?? FOCUS_FILTERS[0]; const FocusIcon = currentFocus.icon; const userLoc: LatLng | null = geo.coords; // Stats: crew count + distinct cities (social proof header). const { cityCount } = useMemo(() => { const cities = new Set( (crews ?? []) .map((c) => c.location?.trim().toLowerCase()) .filter((c): c is string => !!c), ); return { cityCount: cities.size }; }, [crews]); const filtered = useMemo(() => { if (!crews) return []; const q = query.trim().toLowerCase(); let list = crews.filter((c) => { if (focusFilter !== 'all' && c.focus !== focusFilter) return false; if (!q) return true; return ( c.name.toLowerCase().includes(q) || c.location?.toLowerCase().includes(q) || c.bio?.toLowerCase().includes(q) ); }); // Near-me: sort by distance ascending, crews without geo sink to the bottom. if (nearMe && userLoc) { list = [...list].sort((a, b) => { const da = crewDistanceKm(a, userLoc); const db = crewDistanceKm(b, userLoc); if (da === null && db === null) return 0; if (da === null) return 1; if (db === null) return -1; return da - db; }); } return list; }, [crews, query, focusFilter, nearMe, userLoc]); const handleNearMe = () => { if (nearMe) { setNearMe(false); return; } if (!userLoc) geo.request(); setNearMe(true); }; return ( <>
{/* Hero header band */}

{t('socialUi.crews.title')}

{t('socialUi.crews.heroLine')}{' '} {t('socialUi.crews.heroWith')} {t('socialUi.crews.heroRest')}

{/* Social-proof stat line */} {!isLoading && crews && crews.length > 0 && (
{crews.length === 1 ? t('socialUi.crews.crewCountOne', { count: crews.length }) : t('socialUi.crews.crewCountMany', { count: crews.length })} {cityCount > 0 && ( {cityCount === 1 ? t('socialUi.crews.cityCountOne', { count: cityCount }) : t('socialUi.crews.cityCountMany', { count: cityCount })} )}
)}
{/* Search */}
setQuery(e.target.value)} placeholder={t('socialUi.crews.searchPlaceholder')} className="h-10 w-full rounded-xl border border-white/10 bg-white/5 pl-9 pr-9 text-sm text-white placeholder:text-gray-600 transition-all focus:border-teal-500/50 focus:outline-none" /> {query && ( )}
{/* Filters: ONE compact control instead of a horizontally-scrolling chip row (on a phone the chips were clipped and had to be swiped, and the expand toggle read as just another category). */}
{t('socialUi.crews.filterTitle')} {t('socialUi.crews.filterDesc')}
{FOCUS_FILTERS.map(({ key, label, icon: Icon }) => { const active = focusFilter === key; return ( ); })}
{/* Near me — mobile only; on desktop it was just clutter in this bar. */}
{geo.error && nearMe && (

{geo.error}

)} {/* List */} {isLoading ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : filtered.length === 0 ? ( setFocusFilter('all')} onClearSearch={() => setQuery('')} /> ) : (
{filtered.map((crew) => ( ))}
)}
); } function EmptyState({ query, focus, onClearFilter, onClearSearch, }: { query: string; focus: CrewFocus | 'all'; onClearFilter: () => void; onClearSearch: () => void; }) { // 1) A search with no hits — offer a way straight back out. const { t } = useTranslation(); if (query) { return ( ); } // 2) A scene filter with no crews in it — there *are* crews, just not here. // Saying "no crews planted here yet" would be flatly wrong, so offer the way out. if (focus !== 'all') { const label = CREW_FOCUS_KEYS[focus] ? t(CREW_FOCUS_KEYS[focus]) : focus; return ( ); } // 3) Genuinely no crews at all — make it a recruiting loop, not a dead end. // (No "near me" CTA: with zero crews, sorting by distance does nothing.) return ( ); }