- {/* 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. */}
-
+ {/* Tab row — horizontally scrollable pill strip. With 10 crafts a fixed
+ grid would be too cramped, so tabs scroll sideways on small screens and
+ wrap comfortably on wide ones. Icon-over-label stack keeps each tab
+ compact but tappable; a count badge floats in the corner. */}
+
{tabs.map(({ key, label, icon: Icon }) => {
const isActive = serviceType === key;
const a = ACCENT[key];
@@ -294,7 +432,7 @@ function LabToolbar({
key={key}
onClick={() => onServiceChange(key)}
className={cn(
- 'relative flex flex-col items-center justify-center gap-1 px-1 py-2 rounded-xl font-black text-[10px] sm:text-xs uppercase tracking-tight sm:tracking-wide transition-all duration-150 active:scale-95',
+ 'relative flex flex-col items-center justify-center gap-1 px-2.5 py-2 rounded-xl font-black text-[10px] sm:text-xs uppercase tracking-tight sm:tracking-wide transition-all duration-150 active:scale-95 flex-shrink-0 snap-start min-w-[60px]',
isActive
? a.active
: 'text-gray-500 bg-white/5 hover:bg-white/10 hover:text-white border border-transparent'
@@ -488,6 +626,34 @@ function FilterDrawer({
{ 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' },
]
+ : serviceType === 'art'
+ ? [
+ { title: 'What They Make', entries: ARTIST_SERVICE_ENTRIES, activeColor: accent.chip },
+ { title: 'Medium', entries: ARTIST_MEDIUM_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
+ { title: 'Software & Tools', entries: ARTIST_TOOL_ENTRIES, activeColor: 'bg-fuchsia-400 text-white border-fuchsia-400' },
+ ]
+ : serviceType === 'graffiti'
+ ? [
+ { title: 'Style', entries: GRAFFITI_STYLE_ENTRIES, activeColor: accent.chip },
+ { title: 'Medium', entries: GRAFFITI_MEDIUM_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
+ { title: 'Work Context', entries: GRAFFITI_CONTEXT_ENTRIES, activeColor: 'bg-lime-400 text-black border-lime-400' },
+ ]
+ : serviceType === 'tattoo'
+ ? [
+ { title: 'Style', entries: TATTOO_STYLE_ENTRIES, activeColor: accent.chip },
+ { title: 'Method', entries: TATTOO_METHOD_ENTRIES, activeColor: 'bg-rose-400 text-white border-rose-400' },
+ ]
+ : serviceType === 'training'
+ ? [
+ { title: 'Focus', entries: TRAINER_FOCUS_ENTRIES, activeColor: accent.chip },
+ { title: 'Injuries Treated', entries: TRAINER_INJURY_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
+ { title: 'Credentials & Setting', entries: TRAINER_CREDENTIAL_ENTRIES, activeColor: 'bg-emerald-400 text-black border-emerald-400' },
+ ]
+ : serviceType === 'music'
+ ? [
+ { title: 'Genre', entries: MUSICIAN_GENRE_ENTRIES, activeColor: accent.chip },
+ { title: 'Instruments & Role', entries: MUSICIAN_INSTRUMENT_ENTRIES, activeColor: 'bg-sky-400 text-white border-sky-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' },
@@ -660,25 +826,20 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
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 PROFILE_TO_SERVICE: Record = {
+ coach: 'coaching', filmer: 'filming', editor: 'editing', photographer: 'photography', builder: 'building',
+ artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music',
+ };
+ const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType];
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;
+ const TYPE_ICONS: Record> = {
+ coaching: GraduationCap, filming: Video, editing: Scissors, photography: Camera, building: HardHat,
+ art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music,
};
+ const TypeIcon = TYPE_ICONS[serviceType];
+
+ const getSkillLabel = (skill: string) => getSkillLabelForType(skill, serviceType);
return (
@@ -847,6 +1008,46 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
to: '/become-builder',
color: 'bg-amber-500 hover:bg-amber-600 text-black',
},
+ art: {
+ icon: Palette,
+ title: 'No artists yet',
+ body: 'Be the first to offer board graphics, logos, posters and merch for the scene.',
+ cta: 'Offer Design / Art',
+ to: '/become-artist',
+ color: 'bg-fuchsia-600 hover:bg-fuchsia-700',
+ },
+ graffiti: {
+ icon: SprayCan,
+ title: 'No graffiti artists yet',
+ body: 'Be the first to offer pieces, murals and skatepark art in your area.',
+ cta: 'Offer Graffiti',
+ to: '/become-graffiti',
+ color: 'bg-lime-500 hover:bg-lime-600 text-black',
+ },
+ tattoo: {
+ icon: Brush,
+ title: 'No tattoo artists yet',
+ body: 'Be the first skate-culture tattoo artist to list — flash, custom, fine line and more.',
+ cta: 'Offer Tattoo',
+ to: '/become-tattoo',
+ color: 'bg-rose-600 hover:bg-rose-700',
+ },
+ training: {
+ icon: Dumbbell,
+ title: 'No trainers yet',
+ body: 'Be the first trainer or physio to help skaters get strong and stay healthy.',
+ cta: 'Offer Training / Physio',
+ to: '/become-trainer',
+ color: 'bg-emerald-600 hover:bg-emerald-700',
+ },
+ music: {
+ icon: Music,
+ title: 'No musicians yet',
+ body: 'Skate and make music? Add your band or solo project — genres, instruments and links.',
+ cta: 'Add Music Profile',
+ to: '/become-musician',
+ color: 'bg-sky-500 hover:bg-sky-600',
+ },
};
const c = configs[serviceType];
@@ -935,12 +1136,19 @@ export default function LabPage() {
// 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 };
+ const byType = (t: ServiceType) =>
+ profiles.filter((p) => p.profileType === SERVICE_TO_PROFILE_TYPE[t]).length;
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,
+ coaching: byType('coaching'),
+ filming: byType('filming'),
+ photography: byType('photography'),
+ editing: byType('editing'),
+ building: byType('building'),
+ art: byType('art'),
+ graffiti: byType('graffiti'),
+ tattoo: byType('tattoo'),
+ training: byType('training'),
+ music: byType('music'),
};
}, [profiles]);
@@ -948,11 +1156,7 @@ export default function LabPage() {
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;
+ if (profile.profileType !== SERVICE_TO_PROFILE_TYPE[serviceType]) return false;
// Text search: title, bio, and genUserName fallback
if (searchQuery) {