SkateSpots/src/components/LabCrewsStrip.tsx
SkateSpots e357833557 i18n(es): batch 3 part 3 — shared label maps + leftover routing/Sasquatch strings
- labelsUi.* (221+ keys): spot categories/features/types, access, shelter, event
  types, gear categories/conditions, price/listings, tours, crews,
  member-visibility, tag categories, spot status. New src/lib/labelKeys.ts holds
  the shared value->key maps (one source; the five duplicated EVENT_TYPE_KEYS
  copies now import it). spotStatus.statusTimeAgo() uses the locale date shim.
- formatPrice() now takes an optional t so FREE/Trade/Make Offer translate.
- Route helpers: skateTime/distanceReadout/maneuver descriptors/formatAge
  translated (routeUi.*), consumers updated.
- Sasquatch: PRO_TIERS labels/badges + summarizeDraft titles/lines/ctas
  translated (sasquatchUi.*).
- Lab *_LABELS/_DESCRIPTIONS maps remain English (documented out of scope).
2026-10-02 15:31:27 +07:00

122 lines
5.2 KiB
TypeScript

import { Link } from 'react-router-dom';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Users, ChevronRight, MapPin } from 'lucide-react';
import { useCrewsByFocus } from '@/hooks/useCrews';
import { CREW_FOCUS_LABELS, crewFocusForServiceType, type SkateCrew, type CrewFocus } from '@/lib/types';
import { CREW_FOCUS_KEYS } from '@/lib/labelKeys';
import { useTranslation } from '@/contexts/language';
import { Skeleton } from '@/components/ui/skeleton';
/** The Lab page's own ServiceType union (kept in sync with LabPage). */
type ServiceType =
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering';
/** Translate the Lab page's ServiceType into a crew focus scene (or null). */
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> | null {
switch (serviceType) {
// No crew scene maps to judges or organizers — better to show nothing than
// to fall through to "Photo & Film" and imply a wrong association.
case 'judging':
case 'organizing':
case 'barbering':
return null;
case 'art':
case 'graffiti':
case 'tattoo':
return 'art';
case 'music':
return 'music';
case 'training':
return 'wellness';
case 'building':
return crewFocusForServiceType('building');
case 'coaching':
return crewFocusForServiceType('coaching');
default:
return crewFocusForServiceType('filming'); // filming / editing / photography → photo
}
}
function CrewChip({ crew }: { crew: SkateCrew }) {
const { t } = useTranslation();
return (
<Link
to={`/crew/${crew.pubkey}/${crew.identifier}`}
className="group flex w-56 flex-shrink-0 items-center gap-3 rounded-2xl border border-teal-500/20 bg-white/[0.03] p-3 transition-all hover:border-teal-500/50 hover:bg-white/[0.06] active:scale-[0.99]"
>
<Avatar className="h-11 w-11 rounded-xl border border-teal-500/30">
<AvatarImage src={crew.image} alt={crew.name} className="object-cover" />
<AvatarFallback className="rounded-xl bg-teal-900/40 text-teal-300 font-black text-sm">
{crew.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-black text-white leading-tight">{crew.name}</p>
{crew.location ? (
<p className="mt-0.5 flex items-center gap-1 truncate text-xs text-gray-400">
<MapPin className="h-3 w-3 flex-shrink-0 text-teal-400" />
{crew.location}
</p>
) : (
<p className="mt-0.5 flex items-center gap-1 text-xs text-gray-500">
<Users className="h-3 w-3 text-teal-400" />
{t(crew.members.length === 1 ? 'labUi.crewsStrip.memberOne' : 'labUi.crewsStrip.memberMany', { count: crew.members.length })}
</p>
)}
</div>
<ChevronRight className="h-4 w-4 flex-shrink-0 text-gray-600 group-hover:text-teal-400" />
</Link>
);
}
/**
* A horizontal strip of crews that share the active Lab scene's focus.
* Shown beneath the Lab results so crews and individual service providers
* cross-reference each other. Renders nothing when there are no matching crews.
*/
export function LabCrewsStrip({ serviceType }: { serviceType: ServiceType }) {
const { t } = useTranslation();
const focus = focusForLabService(serviceType);
const { data: crews, isLoading } = useCrewsByFocus(focus, 20);
// No matching crew scene for this Lab tab — don't show a strip at all.
if (!focus) return null;
// Only render the section when there's something to show (or while loading).
if (!isLoading && (!crews || crews.length === 0)) return null;
return (
<section className="mt-8">
<div className="mb-3 flex items-center justify-between gap-3">
<h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wide text-teal-400">
<Users className="h-4 w-4" />
{t('labUi.crewsStrip.title')}
<span className="rounded-full bg-teal-500/15 px-2 py-0.5 text-[10px] font-bold text-teal-300">
{CREW_FOCUS_KEYS[focus] ? t(CREW_FOCUS_KEYS[focus]) : CREW_FOCUS_LABELS[focus]}
</span>
</h2>
<Link
to="/crews"
className="flex items-center gap-0.5 text-xs font-bold text-gray-400 hover:text-teal-300"
>
{t('labUi.crewsStrip.allCrews')} <ChevronRight className="h-3.5 w-3.5" />
</Link>
</div>
<div className="-mx-4 flex gap-3 overflow-x-auto px-4 pb-2 scroll-container">
{isLoading
? Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex w-56 flex-shrink-0 items-center gap-3 rounded-2xl border border-white/5 p-3">
<Skeleton className="h-11 w-11 rounded-xl" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
))
: crews?.map((crew) => <CrewChip key={`${crew.pubkey}:${crew.identifier}`} crew={crew} />)}
</div>
</section>
);
}