feat(lab): add Clothing Designer craft (kind 35015)

New data-driven Lab craft, mirroring the artist role: three skill
sections (What You Make [required] / How You Make It / Materials) plus
minimum order, turnaround, size range, samples and customisation extras,
inheriting the shared Lab extras (rate, payment, gallery, reel,
languages, availability, booking).

Wired through types.ts (kind, label/description maps, parse, collective-
capable), labelKeys, useLabProfiles (kind list + dispatch), becomeRoles
(role card), craftFields, LabPage (tab, filters, offer button, empty
state, counts, deep links), LabCrewsStrip, NIP.md, and en/es i18n
(labOptions, becomeRolesUi, labUi). /become/clothing added to the smoke
route list. tsc, eslint, check-i18n, vitest 101/101, build and smoke all
pass.
This commit is contained in:
SkateSpots 2026-10-10 19:30:47 +07:00
parent f00031f5fa
commit 0b645d6d67
13 changed files with 387 additions and 13 deletions

2
NIP.md
View file

@ -1843,6 +1843,7 @@ They share a common base schema and are distinguished by kind and a self-tag.
| 35012 | Event Judge | `skatejudge` | | 35012 | Event Judge | `skatejudge` |
| 35013 | Event Organizer / Host | `skateorganizer` | | 35013 | Event Organizer / Host | `skateorganizer` |
| 35014 | Barber / Hairstylist | `skatebarber` | | 35014 | Barber / Hairstylist | `skatebarber` |
| 35015 | Clothing Designer | `skateclothing` |
> Kind `35006` is **not** a Lab provider — it is the Skate Crew (see below). > Kind `35006` is **not** a Lab provider — it is the Skate Crew (see below).
@ -1925,6 +1926,7 @@ for context; they're stored verbatim and rendered by `src/lib/craftFields.ts`
- **Tattoo**: `deposit`, `minimum` - **Tattoo**: `deposit`, `minimum`
- **Trainer**: `modalities` - **Trainer**: `modalities`
- **Barber**: `duration` - **Barber**: `duration`
- **Clothing**: `minimum`, `turnaround`, `sizing`, `samples`, `customization`
- **Filmer**: `turnaround`, `deliverables`, `drone` - **Filmer**: `turnaround`, `deliverables`, `drone`
- **Editor**: `turnaround`, `deliverables` - **Editor**: `turnaround`, `deliverables`
- **Photographer**: `turnaround`, `deliverables` - **Photographer**: `turnaround`, `deliverables`

View file

@ -134,6 +134,7 @@ const ROUTES = [
'/lab', '/lab',
'/become/organizer', '/become/organizer',
'/become/barber', '/become/barber',
'/become/clothing',
'/leaderboards', '/leaderboards',
'/bounties', '/bounties',
'/settings', '/settings',

View file

@ -10,7 +10,7 @@ import { Skeleton } from '@/components/ui/skeleton';
/** The Lab page's own ServiceType union (kept in sync with LabPage). */ /** The Lab page's own ServiceType union (kept in sync with LabPage). */
type ServiceType = type ServiceType =
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building' | 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering'; | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering' | 'clothing';
/** Translate the Lab page's ServiceType into a crew focus scene (or null). */ /** Translate the Lab page's ServiceType into a crew focus scene (or null). */
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> | null { function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> | null {
@ -24,6 +24,7 @@ function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed
case 'art': case 'art':
case 'graffiti': case 'graffiti':
case 'tattoo': case 'tattoo':
case 'clothing':
return 'art'; return 'art';
case 'music': case 'music':
return 'music'; return 'music';

View file

@ -2,14 +2,14 @@ import { useQuery } from '@tanstack/react-query';
import { import {
SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, SKATE_BUILDER_KIND, 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, SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_KIND,
SKATE_BARBER_KIND, SKATE_BARBER_KIND, SKATE_DESIGNER_KIND,
parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder, parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder,
parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, parseSkateOrganizer, parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, parseSkateOrganizer,
parseSkateBarber, parseSkateBarber, parseSkateDesigner,
isFeaturedActive, isFeaturedActive,
type SkateCoach, type SkateFilmer, type SkateEditor, type SkatePhotographer, type SkateBuilder, type SkateCoach, type SkateFilmer, type SkateEditor, type SkatePhotographer, type SkateBuilder,
type SkateArtist, type SkateGraffiti, type SkateTattoo, type SkateTrainer, type SkateMusician, type SkateJudge, type SkateOrganizer, type SkateArtist, type SkateGraffiti, type SkateTattoo, type SkateTrainer, type SkateMusician, type SkateJudge, type SkateOrganizer,
type SkateBarber, type SkateBarber, type SkateDesigner,
} from '@/lib/types'; } from '@/lib/types';
import type { NostrEvent } from '@nostrify/nostrify'; import type { NostrEvent } from '@nostrify/nostrify';
import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useSkateRelays } from '@/hooks/useSkateRelays';
@ -18,12 +18,12 @@ import { verifiedFeaturedCoords } from '@/lib/featuredVerification';
export type LabProfileType = export type LabProfileType =
| 'coach' | 'filmer' | 'editor' | 'photographer' | 'builder' | 'coach' | 'filmer' | 'editor' | 'photographer' | 'builder'
| 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge' | 'organizer' | 'barber'; | 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge' | 'organizer' | 'barber' | 'designer';
export type LabProfile = ( export type LabProfile = (
| SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder | SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder
| SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge | SkateOrganizer | SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge | SkateOrganizer
| SkateBarber | SkateBarber | SkateDesigner
) & { ) & {
profileType: LabProfileType; profileType: LabProfileType;
}; };
@ -31,13 +31,13 @@ export type LabProfile = (
const LAB_KINDS = [ const LAB_KINDS = [
SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, SKATE_BUILDER_KIND, 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, SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_KIND,
SKATE_BARBER_KIND, SKATE_BARBER_KIND, SKATE_DESIGNER_KIND,
]; ];
const LAB_SELF_TAGS = [ const LAB_SELF_TAGS = [
'skatecoach', 'skatefilmer', 'skateeditor', 'skatephotographer', 'skatebuilder', 'skatecoach', 'skatefilmer', 'skateeditor', 'skatephotographer', 'skatebuilder',
'skateartist', 'skategraffiti', 'skatetattoo', 'skatetrainer', 'skatemusician', 'skatejudge', 'skateorganizer', 'skateartist', 'skategraffiti', 'skatetattoo', 'skatetrainer', 'skatemusician', 'skatejudge', 'skateorganizer',
'skatebarber', 'skatebarber', 'skateclothing',
]; ];
/** profileType → addressable kind, for building a coordinate. */ /** profileType → addressable kind, for building a coordinate. */
@ -46,7 +46,7 @@ const KIND_BY_TYPE: Record<LabProfileType, number> = {
photographer: SKATE_PHOTOGRAPHER_KIND, builder: SKATE_BUILDER_KIND, artist: SKATE_ARTIST_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, graffiti: SKATE_GRAFFITI_KIND, tattoo: SKATE_TATTOO_KIND, trainer: SKATE_TRAINER_KIND,
musician: SKATE_MUSICIAN_KIND, judge: SKATE_JUDGE_KIND, organizer: SKATE_ORGANIZER_KIND, musician: SKATE_MUSICIAN_KIND, judge: SKATE_JUDGE_KIND, organizer: SKATE_ORGANIZER_KIND,
barber: SKATE_BARBER_KIND, barber: SKATE_BARBER_KIND, designer: SKATE_DESIGNER_KIND,
}; };
/** Parse any Lab event into a LabProfile, or null if invalid / unknown kind. */ /** Parse any Lab event into a LabProfile, or null if invalid / unknown kind. */
@ -104,6 +104,10 @@ export function parseLabProfile(event: NostrEvent): LabProfile | null {
const p = parseSkateBarber(event); const p = parseSkateBarber(event);
return p ? { ...p, profileType: 'barber' } : null; return p ? { ...p, profileType: 'barber' } : null;
} }
case SKATE_DESIGNER_KIND: {
const p = parseSkateDesigner(event);
return p ? { ...p, profileType: 'designer' } : null;
}
default: default:
return null; return null;
} }

View file

@ -554,6 +554,60 @@ const labOptions = {
event: 'Skate Events & Comps', event: 'Skate Events & Comps',
}, },
}, },
clothing: {
service: {
tees: 'T-Shirts',
hoodies: 'Hoodies & Sweats',
pants: 'Pants & Shorts',
jackets: 'Jackets & Outerwear',
kits: 'Team Kits & Jerseys',
caps: 'Caps & Beanies',
socks: 'Socks',
accessories: 'Bags & Accessories',
footwear: 'Footwear & Shoes',
patches: 'Patches & Appliqué',
griptape: 'Griptape & Deck Apparel',
onepiece: 'One-of-One / Upcycled',
},
serviceDesc: {
tees: 'Custom tees and tops',
hoodies: 'Hoodies, crews and sweatpants',
pants: 'Work pants, cargos and shorts',
jackets: 'Coaches, shells and work jackets',
kits: 'Team kits, jerseys and uniforms',
caps: 'Caps, beanies and headwear',
socks: 'Crew socks and knitwear',
accessories: 'Bags, belts and small goods',
footwear: 'Custom shoes and footwear',
patches: 'Embroidered patches and appliqué',
griptape: 'Griptape art and deck apparel',
onepiece: 'Single pieces and upcycled garments',
},
method: {
screenprint: 'Screen Print',
dtg: 'DTG / Direct-to-Garment',
embroidery: 'Embroidery',
cutsew: 'Cut & Sew',
sublimation: 'Sublimation',
heattransfer: 'Heat Transfer / Vinyl',
garmentdye: 'Garment Dye',
knit: 'Knitwear',
patchwork: 'Patchwork',
allover: 'All-over Print',
},
material: {
cotton: 'Cotton',
heavyweight: 'Heavyweight',
organic: 'Organic / Sustainable',
fleece: 'Fleece',
denim: 'Denim',
canvas: 'Canvas',
nylon: 'Nylon / Technical',
wool: 'Merino / Wool',
leather: 'Leather',
recycled: 'Recycled / Deadstock',
},
},
tour: { tour: {
skill: { skill: {
beginner: { label: 'Beginner', description: 'New to skating or basic tricks' }, beginner: { label: 'Beginner', description: 'New to skating or basic tricks' },
@ -1110,6 +1164,60 @@ const labOptions = {
event: 'Eventos y competiciones de skate', event: 'Eventos y competiciones de skate',
}, },
}, },
clothing: {
service: {
tees: 'Camisetas',
hoodies: 'Sudaderas y jerséis',
pants: 'Pantalones y shorts',
jackets: 'Chaquetas y abrigos',
kits: 'Equipaciones y camisetas',
caps: 'Gorras y gorros',
socks: 'Calcetines',
accessories: 'Bolsos y accesorios',
footwear: 'Calzado y zapatillas',
patches: 'Parches y apliques',
griptape: 'Lija y ropa de tabla',
onepiece: 'Piezas únicas / upcycled',
},
serviceDesc: {
tees: 'Camisetas y tops personalizados',
hoodies: 'Sudaderas, jerséis y pantalones de chándal',
pants: 'Pantalones de trabajo, cargo y shorts',
jackets: 'Chaquetas de entrenador, cortavientos y de trabajo',
kits: 'Equipaciones, camisetas y uniformes de equipo',
caps: 'Gorras, gorros y prendas para la cabeza',
socks: 'Calcetines y punto',
accessories: 'Bolsos, cinturones y pequeños complementos',
footwear: 'Zapatos y calzado personalizado',
patches: 'Parches bordados y apliques',
griptape: 'Arte en lija y ropa de tabla',
onepiece: 'Piezas únicas y prendas upcycled',
},
method: {
screenprint: 'Serigrafía',
dtg: 'DTG / impresión directa',
embroidery: 'Bordado',
cutsew: 'Corte y confección',
sublimation: 'Sublimación',
heattransfer: 'Transfer / vinilo',
garmentdye: 'Tinte de prenda',
knit: 'Punto',
patchwork: 'Patchwork',
allover: 'Estampado integral',
},
material: {
cotton: 'Algodón',
heavyweight: 'Peso pesado',
organic: 'Orgánico / sostenible',
fleece: 'Polar',
denim: 'Denim',
canvas: 'Lona',
nylon: 'Nailon / técnico',
wool: 'Merino / lana',
leather: 'Cuero',
recycled: 'Reciclado / deadstock',
},
},
tour: { tour: {
skill: { skill: {
beginner: { label: 'Principiante', description: 'Nuevo en el skate o trucos básicos' }, beginner: { label: 'Principiante', description: 'Nuevo en el skate o trucos básicos' },

View file

@ -2113,6 +2113,7 @@ labUi: {
offer: 'Offer', offer: 'Offer',
offerBuilding: 'Offer Building', offerBuilding: 'Offer Building',
offerCoaching: 'Offer Coaching', offerCoaching: 'Offer Coaching',
offerClothing: 'Offer Clothing',
offerCuts: 'Offer Cuts', offerCuts: 'Offer Cuts',
offerDesignArt: 'Offer Design / Art', offerDesignArt: 'Offer Design / Art',
offerEditing: 'Offer Editing', offerEditing: 'Offer Editing',
@ -2183,6 +2184,7 @@ labUi: {
tabJudges: 'Judges', tabJudges: 'Judges',
tabOrganizers: 'Organizers', tabOrganizers: 'Organizers',
tabBarbers: 'Barbers', tabBarbers: 'Barbers',
tabClothing: 'Clothing',
filterSkillsToLearn: 'Skills to Learn', filterSkillsToLearn: 'Skills to Learn',
filterContentType: 'Content Type', filterContentType: 'Content Type',
filterEquipmentStyle: 'Equipment & Style', filterEquipmentStyle: 'Equipment & Style',
@ -2252,6 +2254,8 @@ labUi: {
organizersBody: 'Put on jams, comps, meetups or demos? List yourself so skaters can find and book you.', organizersBody: 'Put on jams, comps, meetups or demos? List yourself so skaters can find and book you.',
barbersTitle: 'No barbers yet', barbersTitle: 'No barbers yet',
barbersBody: 'Cut hair for skaters? List your chair or mobile service so the scene can get a fresh trim.', barbersBody: 'Cut hair for skaters? List your chair or mobile service so the scene can get a fresh trim.',
clothingTitle: 'No clothing designers yet',
clothingBody: 'Be the first to offer custom clothes, team kits and apparel for the scene.',
}, },
become: { become: {
addNip05: 'Add NIP-05 in Settings', addNip05: 'Add NIP-05 in Settings',
@ -2895,6 +2899,9 @@ labUi: {
drone: 'Drone', drone: 'Drone',
leadTime: 'Lead Time', leadTime: 'Lead Time',
warranty: 'Warranty', warranty: 'Warranty',
sizing: 'Size Range',
samples: 'Samples / Prototypes',
customization: 'Customisation',
}, },
craftForm: { craftForm: {
profileImage: 'Profile image', profileImage: 'Profile image',

View file

@ -2113,6 +2113,7 @@ labUi: {
offer: 'Ofrecer', offer: 'Ofrecer',
offerBuilding: 'Ofrecer construcción', offerBuilding: 'Ofrecer construcción',
offerCoaching: 'Ofrecer clases', offerCoaching: 'Ofrecer clases',
offerClothing: 'Ofrecer ropa',
offerCuts: 'Ofrecer cortes', offerCuts: 'Ofrecer cortes',
offerDesignArt: 'Ofrecer diseño / arte', offerDesignArt: 'Ofrecer diseño / arte',
offerEditing: 'Ofrecer edición', offerEditing: 'Ofrecer edición',
@ -2183,6 +2184,7 @@ labUi: {
tabJudges: 'Jueces', tabJudges: 'Jueces',
tabOrganizers: 'Organizadores', tabOrganizers: 'Organizadores',
tabBarbers: 'Barberos', tabBarbers: 'Barberos',
tabClothing: 'Ropa',
filterSkillsToLearn: 'Trucos que enseñar', filterSkillsToLearn: 'Trucos que enseñar',
filterContentType: 'Tipo de contenido', filterContentType: 'Tipo de contenido',
filterEquipmentStyle: 'Equipo y estilo', filterEquipmentStyle: 'Equipo y estilo',
@ -2252,6 +2254,8 @@ labUi: {
organizersBody: '¿Organizas quedadas, competiciones, encuentros o demos? Apúntate para que los skaters puedan encontrarte y contratarte.', organizersBody: '¿Organizas quedadas, competiciones, encuentros o demos? Apúntate para que los skaters puedan encontrarte y contratarte.',
barbersTitle: 'Aún no hay barberos', barbersTitle: 'Aún no hay barberos',
barbersBody: '¿Cortas el pelo a skaters? Añade tu silla o servicio móvil para que la escena se dé un retoque.', barbersBody: '¿Cortas el pelo a skaters? Añade tu silla o servicio móvil para que la escena se dé un retoque.',
clothingTitle: 'Aún no hay diseñadores de ropa',
clothingBody: 'Sé el primero en ofrecer ropa personalizada, equipaciones y prendas para la escena.',
}, },
become: { become: {
addNip05: 'Añade NIP-05 en Ajustes', addNip05: 'Añade NIP-05 en Ajustes',
@ -2895,6 +2899,9 @@ labUi: {
drone: 'Dron', drone: 'Dron',
leadTime: 'Plazo de entrega', leadTime: 'Plazo de entrega',
warranty: 'Garantía', warranty: 'Garantía',
sizing: 'Tallas',
samples: 'Muestras / prototipos',
customization: 'Personalización',
}, },
craftForm: { craftForm: {
profileImage: 'Imagen de perfil', profileImage: 'Imagen de perfil',

View file

@ -149,6 +149,23 @@ const becomeRolesUi = {
booksOpenHelper: 'Are you currently taking bookings?', booksOpenHelper: 'Are you currently taking bookings?',
durationPlaceholder: 'e.g. 30–45 min', durationPlaceholder: 'e.g. 30–45 min',
}, },
clothing: {
seoTitle: 'Become a Clothing Designer - SkateSpots',
seoDescription: 'Custom clothes, team kits & apparel for the scene.',
heading: 'Clothing Designer',
tagline: 'Custom clothes, team kits & apparel for the scene',
noun: 'clothing designer',
rateLabel: 'Rate',
portfolioLabel: 'Shop / portfolio link',
sectionWhatYouMake: 'What You Make',
sectionHowYouMake: 'How You Make It',
sectionMaterials: 'Materials',
minimumPlaceholder: 'e.g. 10 pieces',
turnaroundPlaceholder: 'e.g. 2-3 weeks',
sizingPlaceholder: 'e.g. XS-3XL / made to measure',
samplesHelper: 'Do you offer samples or prototypes before a full run?',
customizationPlaceholder: 'e.g. names, numbers, graphics',
},
}, },
es: { es: {
artist: { artist: {
@ -290,6 +307,23 @@ const becomeRolesUi = {
booksOpenHelper: '¿Estás aceptando reservas ahora mismo?', booksOpenHelper: '¿Estás aceptando reservas ahora mismo?',
durationPlaceholder: 'p. ej. 30–45 min', durationPlaceholder: 'p. ej. 30–45 min',
}, },
clothing: {
seoTitle: 'Conviértete en diseñador de ropa - SkateSpots',
seoDescription: 'Ropa a medida, equipaciones y prendas para la escena.',
heading: 'Diseñador de ropa',
tagline: 'Ropa a medida, equipaciones y prendas para la escena',
noun: 'diseñador de ropa',
rateLabel: 'Tarifa',
portfolioLabel: 'Enlace de tienda / portafolio',
sectionWhatYouMake: 'Qué haces',
sectionHowYouMake: 'Cómo lo haces',
sectionMaterials: 'Materiales',
minimumPlaceholder: 'p. ej. 10 prendas',
turnaroundPlaceholder: 'p. ej. 2-3 semanas',
sizingPlaceholder: 'p. ej. XS-3XL / a medida',
samplesHelper: '¿Ofreces muestras o prototipos antes de una producción completa?',
customizationPlaceholder: 'p. ej. nombres, números, gráficos',
},
}, },
}; };

View file

@ -1,6 +1,6 @@
import { lazy, type ComponentType, type LazyExoticComponent } from 'react'; import { lazy, type ComponentType, type LazyExoticComponent } from 'react';
import { import {
Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent, Sparkles, Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent, Sparkles, Shirt,
} from 'lucide-react'; } from 'lucide-react';
import { pickRateUnits } from '@/lib/labExtras'; import { pickRateUnits } from '@/lib/labExtras';
import type { CraftFormConfig } from '@/components/CraftProfileForm'; import type { CraftFormConfig } from '@/components/CraftProfileForm';
@ -13,6 +13,7 @@ import {
SKATE_TATTOO_KIND, TATTOO_STYLE_LABELS, TATTOO_METHOD_LABELS, SKATE_TATTOO_KIND, TATTOO_STYLE_LABELS, TATTOO_METHOD_LABELS,
SKATE_TRAINER_KIND, TRAINER_FOCUS_LABELS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS, SKATE_TRAINER_KIND, TRAINER_FOCUS_LABELS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS,
SKATE_BARBER_KIND, BARBER_SERVICE_LABELS, BARBER_STYLE_LABELS, BARBER_SETTING_LABELS, SKATE_BARBER_KIND, BARBER_SERVICE_LABELS, BARBER_STYLE_LABELS, BARBER_SETTING_LABELS,
SKATE_DESIGNER_KIND, DESIGNER_SERVICE_LABELS, DESIGNER_METHOD_LABELS, DESIGNER_MATERIAL_LABELS,
} from '@/lib/types'; } from '@/lib/types';
import { import {
ARTIST_SERVICE_KEYS, ARTIST_MEDIUM_KEYS, ARTIST_TOOL_KEYS, ARTIST_SERVICE_KEYS, ARTIST_MEDIUM_KEYS, ARTIST_TOOL_KEYS,
@ -23,6 +24,7 @@ import {
TATTOO_STYLE_KEYS, TATTOO_METHOD_KEYS, TATTOO_STYLE_KEYS, TATTOO_METHOD_KEYS,
TRAINER_FOCUS_KEYS, TRAINER_INJURY_KEYS, TRAINER_CREDENTIAL_KEYS, TRAINER_FOCUS_KEYS, TRAINER_INJURY_KEYS, TRAINER_CREDENTIAL_KEYS,
BARBER_SERVICE_KEYS, BARBER_STYLE_KEYS, BARBER_SETTING_KEYS, BARBER_SERVICE_KEYS, BARBER_STYLE_KEYS, BARBER_SETTING_KEYS,
DESIGNER_SERVICE_KEYS, DESIGNER_METHOD_KEYS, DESIGNER_MATERIAL_KEYS,
} from '@/lib/labelKeys'; } from '@/lib/labelKeys';
/** /**
@ -375,6 +377,46 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
], ],
}, },
}, },
clothing: {
kind: 'craft',
seoTitle: 'Become a Clothing Designer - SkateSpots',
seoDescription: 'Custom clothes, team kits & apparel for the scene.',
seoTitleKey: 'becomeRolesUi.clothing.seoTitle',
seoDescriptionKey: 'becomeRolesUi.clothing.seoDescription',
config: {
kind: SKATE_DESIGNER_KIND,
selfTag: 'skateclothing',
identifier: 'clothing-profile',
heading: 'Clothing Designer',
headingKey: 'becomeRolesUi.clothing.heading',
tagline: 'Custom clothes, team kits & apparel for the scene',
taglineKey: 'becomeRolesUi.clothing.tagline',
noun: 'clothing designer',
nounKey: 'becomeRolesUi.clothing.noun',
accentText: 'text-violet-400',
accentBg: 'bg-violet-600',
accentChip: 'bg-violet-600 text-white border-violet-600',
accentChipHover: 'hover:border-violet-500/60 hover:text-violet-400',
icon: Shirt,
rateUnits: pickRateUnits('piece', 'project', 'hour', 'day'),
extraFields: [
{ tag: 'minimum', label: 'Minimum', labelKey: 'labUi.craftFields.minimum', placeholder: 'e.g. 10 pieces', placeholderKey: 'becomeRolesUi.clothing.minimumPlaceholder' },
{ tag: 'turnaround', label: 'Turnaround', labelKey: 'labUi.craftFields.turnaround', placeholder: 'e.g. 2-3 weeks', placeholderKey: 'becomeRolesUi.clothing.turnaroundPlaceholder' },
{ tag: 'sizing', label: 'Size Range', labelKey: 'labUi.craftFields.sizing', placeholder: 'e.g. XS-3XL / made to measure', placeholderKey: 'becomeRolesUi.clothing.sizingPlaceholder' },
{ tag: 'samples', label: 'Samples / Prototypes', labelKey: 'labUi.craftFields.samples', type: 'toggle', helper: 'Do you offer samples or prototypes before a full run?', helperKey: 'becomeRolesUi.clothing.samplesHelper' },
{ tag: 'customization', label: 'Customisation', labelKey: 'labUi.craftFields.customization', placeholder: 'e.g. names, numbers, graphics', placeholderKey: 'becomeRolesUi.clothing.customizationPlaceholder' },
],
rateLabel: 'Rate',
rateLabelKey: 'becomeRolesUi.clothing.rateLabel',
portfolioLabel: 'Shop / portfolio link',
portfolioLabelKey: 'becomeRolesUi.clothing.portfolioLabel',
skillSections: [
{ title: 'What You Make', titleKey: 'becomeRolesUi.clothing.sectionWhatYouMake', entries: Object.entries(DESIGNER_SERVICE_LABELS), keyMap: DESIGNER_SERVICE_KEYS, required: true },
{ title: 'How You Make It', titleKey: 'becomeRolesUi.clothing.sectionHowYouMake', entries: Object.entries(DESIGNER_METHOD_LABELS), keyMap: DESIGNER_METHOD_KEYS },
{ title: 'Materials', titleKey: 'becomeRolesUi.clothing.sectionMaterials', entries: Object.entries(DESIGNER_MATERIAL_LABELS), keyMap: DESIGNER_MATERIAL_KEYS },
],
},
},
}; };
export function getBecomeRole(slug: string | undefined): BecomeRole | undefined { export function getBecomeRole(slug: string | undefined): BecomeRole | undefined {

View file

@ -44,6 +44,13 @@ export const CRAFT_DETAIL_FIELDS: Partial<Record<LabProfileType, CraftDetailFiel
barber: [ barber: [
{ tag: 'duration', labelKey: 'labUi.craftFields.duration' }, { tag: 'duration', labelKey: 'labUi.craftFields.duration' },
], ],
designer: [
{ tag: 'minimum', labelKey: 'labUi.craftFields.minimum' },
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
{ tag: 'sizing', labelKey: 'labUi.craftFields.sizing' },
{ tag: 'samples', labelKey: 'labUi.craftFields.samples' },
{ tag: 'customization', labelKey: 'labUi.craftFields.customization' },
],
filmer: [ filmer: [
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' }, { tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' }, { tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },

View file

@ -627,6 +627,47 @@ export const ARTIST_TOOL_KEYS: Record<string, string> = {
clipstudio: 'labOptions.artist.tool.clipstudio', clipstudio: 'labOptions.artist.tool.clipstudio',
}; };
export const DESIGNER_SERVICE_KEYS: Record<string, string> = {
tees: 'labOptions.clothing.service.tees',
hoodies: 'labOptions.clothing.service.hoodies',
pants: 'labOptions.clothing.service.pants',
jackets: 'labOptions.clothing.service.jackets',
kits: 'labOptions.clothing.service.kits',
caps: 'labOptions.clothing.service.caps',
socks: 'labOptions.clothing.service.socks',
accessories: 'labOptions.clothing.service.accessories',
footwear: 'labOptions.clothing.service.footwear',
patches: 'labOptions.clothing.service.patches',
griptape: 'labOptions.clothing.service.griptape',
onepiece: 'labOptions.clothing.service.onepiece',
};
export const DESIGNER_METHOD_KEYS: Record<string, string> = {
screenprint: 'labOptions.clothing.method.screenprint',
dtg: 'labOptions.clothing.method.dtg',
embroidery: 'labOptions.clothing.method.embroidery',
cutsew: 'labOptions.clothing.method.cutsew',
sublimation: 'labOptions.clothing.method.sublimation',
heattransfer: 'labOptions.clothing.method.heattransfer',
garmentdye: 'labOptions.clothing.method.garmentdye',
knit: 'labOptions.clothing.method.knit',
patchwork: 'labOptions.clothing.method.patchwork',
allover: 'labOptions.clothing.method.allover',
};
export const DESIGNER_MATERIAL_KEYS: Record<string, string> = {
cotton: 'labOptions.clothing.material.cotton',
heavyweight: 'labOptions.clothing.material.heavyweight',
organic: 'labOptions.clothing.material.organic',
fleece: 'labOptions.clothing.material.fleece',
denim: 'labOptions.clothing.material.denim',
canvas: 'labOptions.clothing.material.canvas',
nylon: 'labOptions.clothing.material.nylon',
wool: 'labOptions.clothing.material.wool',
leather: 'labOptions.clothing.material.leather',
recycled: 'labOptions.clothing.material.recycled',
};
export const GRAFFITI_STYLE_KEYS: Record<string, string> = { export const GRAFFITI_STYLE_KEYS: Record<string, string> = {
throwups: 'labOptions.graffiti.style.throwups', throwups: 'labOptions.graffiti.style.throwups',
pieces: 'labOptions.graffiti.style.pieces', pieces: 'labOptions.graffiti.style.pieces',

View file

@ -70,6 +70,7 @@ export const SKATE_MUSICIAN_KIND = 35011; // Musician / band profiles (skaters w
export const SKATE_JUDGE_KIND = 35012; // Skate judges (event officials) export const SKATE_JUDGE_KIND = 35012; // Skate judges (event officials)
export const SKATE_ORGANIZER_KIND = 35013; // Event organizers / hosts (jams, comps, meetups) export const SKATE_ORGANIZER_KIND = 35013; // Event organizers / hosts (jams, comps, meetups)
export const SKATE_BARBER_KIND = 35014; // Barbers / hairstylists (skater-friendly cuts) export const SKATE_BARBER_KIND = 35014; // Barbers / hairstylists (skater-friendly cuts)
export const SKATE_DESIGNER_KIND = 35015; // Clothing designers (custom clothes, team kits, apparel)
export const MARKETPLACE_ITEM_KIND = 30402; // NIP-99 Classified Listings export const MARKETPLACE_ITEM_KIND = 30402; // NIP-99 Classified Listings
export const SPOT_COLLECTION_KIND = 30003; // NIP-51 Bookmark sets for spot collections export const SPOT_COLLECTION_KIND = 30003; // NIP-51 Bookmark sets for spot collections
export const SKATE_TOUR_KIND = 30823; // Local skate tours - guides offer to show hidden spots export const SKATE_TOUR_KIND = 30823; // Local skate tours - guides offer to show hidden spots
@ -488,6 +489,7 @@ const COLLECTIVE_CAPABLE_KINDS = new Set<number>([
SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, 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_BUILDER_KIND, SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND,
SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND,
SKATE_DESIGNER_KIND,
]); ]);
export function parseCollectiveJoinRequest(event: NostrEvent): CollectiveJoinRequest | null { export function parseCollectiveJoinRequest(event: NostrEvent): CollectiveJoinRequest | null {
@ -1699,6 +1701,67 @@ export const ARTIST_TOOL_LABELS: Record<string, string> = {
clipstudio: 'Clip Studio', clipstudio: 'Clip Studio',
}; };
// ----- Clothing designer (custom clothes, team kits, apparel) -----
// What they make
export const DESIGNER_SERVICE_LABELS: Record<string, string> = {
tees: 'T-Shirts',
hoodies: 'Hoodies & Sweats',
pants: 'Pants & Shorts',
jackets: 'Jackets & Outerwear',
kits: 'Team Kits & Jerseys',
caps: 'Caps & Beanies',
socks: 'Socks',
accessories: 'Bags & Accessories',
footwear: 'Footwear & Shoes',
patches: 'Patches & Appliqué',
griptape: 'Griptape & Deck Apparel',
onepiece: 'One-of-One / Upcycled',
};
export const DESIGNER_SERVICE_DESCRIPTIONS: Record<string, string> = {
tees: 'Custom tees and tops',
hoodies: 'Hoodies, crews and sweatpants',
pants: 'Work pants, cargos and shorts',
jackets: 'Coaches, shells and work jackets',
kits: 'Team kits, jerseys and uniforms',
caps: 'Caps, beanies and headwear',
socks: 'Crew socks and knitwear',
accessories: 'Bags, belts and small goods',
footwear: 'Custom shoes and footwear',
patches: 'Embroidered patches and appliqué',
griptape: 'Griptape art and deck apparel',
onepiece: 'Single pieces and upcycled garments',
};
// How they make it
export const DESIGNER_METHOD_LABELS: Record<string, string> = {
screenprint: 'Screen Print',
dtg: 'DTG / Direct-to-Garment',
embroidery: 'Embroidery',
cutsew: 'Cut & Sew',
sublimation: 'Sublimation',
heattransfer: 'Heat Transfer / Vinyl',
garmentdye: 'Garment Dye',
knit: 'Knitwear',
patchwork: 'Patchwork',
allover: 'All-over Print',
};
// Materials
export const DESIGNER_MATERIAL_LABELS: Record<string, string> = {
cotton: 'Cotton',
heavyweight: 'Heavyweight',
organic: 'Organic / Sustainable',
fleece: 'Fleece',
denim: 'Denim',
canvas: 'Canvas',
nylon: 'Nylon / Technical',
wool: 'Merino / Wool',
leather: 'Leather',
recycled: 'Recycled / Deadstock',
};
// ----- Graffiti / Mural artist ----- // ----- Graffiti / Mural artist -----
// Styles // Styles
@ -2415,6 +2478,8 @@ export interface SkateArtist {
event: NostrEvent; event: NostrEvent;
} }
export type SkateDesigner = SkateArtist;
export interface SkateGraffiti { export interface SkateGraffiti {
id: string; id: string;
pubkey: string; pubkey: string;
@ -3065,6 +3130,13 @@ export function parseSkateArtist(event: NostrEvent): SkateArtist | null {
return base; return base;
} }
export function parseSkateDesigner(event: NostrEvent): SkateDesigner | null {
if (event.kind !== SKATE_DESIGNER_KIND) return null;
const base = parseCraftBase(event, 'skateclothing');
if (!base) return null;
return base;
}
export function parseSkateGraffiti(event: NostrEvent): SkateGraffiti | null { export function parseSkateGraffiti(event: NostrEvent): SkateGraffiti | null {
if (event.kind !== SKATE_GRAFFITI_KIND) return null; if (event.kind !== SKATE_GRAFFITI_KIND) return null;
const base = parseCraftBase(event, 'skategraffiti'); const base = parseCraftBase(event, 'skategraffiti');

View file

@ -52,6 +52,9 @@ import {
BARBER_SERVICE_LABELS, BARBER_SERVICE_LABELS,
BARBER_STYLE_LABELS, BARBER_STYLE_LABELS,
BARBER_SETTING_LABELS, BARBER_SETTING_LABELS,
DESIGNER_SERVICE_LABELS,
DESIGNER_METHOD_LABELS,
DESIGNER_MATERIAL_LABELS,
} from '@/lib/types'; } from '@/lib/types';
import { import {
COACH_SKILL_KEYS, COACH_SKILL_KEYS,
@ -90,6 +93,9 @@ import {
BARBER_SERVICE_KEYS, BARBER_SERVICE_KEYS,
BARBER_STYLE_KEYS, BARBER_STYLE_KEYS,
BARBER_SETTING_KEYS, BARBER_SETTING_KEYS,
DESIGNER_SERVICE_KEYS,
DESIGNER_METHOD_KEYS,
DESIGNER_MATERIAL_KEYS,
} from '@/lib/labelKeys'; } from '@/lib/labelKeys';
import { import {
GraduationCap, GraduationCap,
@ -120,6 +126,7 @@ import {
Tent, Tent,
Sparkles, Sparkles,
Pencil, Pencil,
Shirt,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { EmptyState as SharedEmptyState } from '@/components/EmptyState'; import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
@ -204,6 +211,13 @@ const BARBER_ALL_LABELS: Record<string, string> = {
...BARBER_SERVICE_LABELS, ...BARBER_STYLE_LABELS, ...BARBER_SETTING_LABELS, ...BARBER_SERVICE_LABELS, ...BARBER_STYLE_LABELS, ...BARBER_SETTING_LABELS,
}; };
const DESIGNER_SERVICE_ENTRIES = Object.entries(DESIGNER_SERVICE_LABELS);
const DESIGNER_METHOD_ENTRIES = Object.entries(DESIGNER_METHOD_LABELS);
const DESIGNER_MATERIAL_ENTRIES = Object.entries(DESIGNER_MATERIAL_LABELS);
const DESIGNER_ALL_LABELS: Record<string, string> = {
...DESIGNER_SERVICE_LABELS, ...DESIGNER_METHOD_LABELS, ...DESIGNER_MATERIAL_LABELS,
};
// All photographer label maps merged — for resolving a chip label regardless of category // All photographer label maps merged — for resolving a chip label regardless of category
const PHOTO_ALL_LABELS: Record<string, string> = { const PHOTO_ALL_LABELS: Record<string, string> = {
...PHOTO_SERVICE_LABELS, ...PHOTO_SERVICE_LABELS,
@ -216,7 +230,8 @@ const PHOTO_ALL_LABELS: Record<string, string> = {
type ServiceType = type ServiceType =
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building' | 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering'; | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering'
| 'clothing';
/** Map a ServiceType to the LabProfile.profileType it filters. */ /** Map a ServiceType to the LabProfile.profileType it filters. */
const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> = { const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> = {
@ -233,6 +248,7 @@ const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> =
judging: 'judge', judging: 'judge',
organizing: 'organizer', organizing: 'organizer',
barbering: 'barber', barbering: 'barber',
clothing: 'designer',
}; };
/** Lab option translation keys per service type (merged across each craft's maps). */ /** Lab option translation keys per service type (merged across each craft's maps). */
@ -250,6 +266,7 @@ const KEYS_BY_TYPE: Record<ServiceType, Record<string, string>> = {
judging: { ...JUDGE_FORMAT_KEYS, ...JUDGE_SPECIALTY_KEYS, ...JUDGE_LEVEL_KEYS }, judging: { ...JUDGE_FORMAT_KEYS, ...JUDGE_SPECIALTY_KEYS, ...JUDGE_LEVEL_KEYS },
organizing: { ...ORGANIZER_EVENT_KEYS, ...ORGANIZER_SERVICE_KEYS, ...ORGANIZER_SCALE_KEYS }, organizing: { ...ORGANIZER_EVENT_KEYS, ...ORGANIZER_SERVICE_KEYS, ...ORGANIZER_SCALE_KEYS },
barbering: { ...BARBER_SERVICE_KEYS, ...BARBER_STYLE_KEYS, ...BARBER_SETTING_KEYS }, barbering: { ...BARBER_SERVICE_KEYS, ...BARBER_STYLE_KEYS, ...BARBER_SETTING_KEYS },
clothing: { ...DESIGNER_SERVICE_KEYS, ...DESIGNER_METHOD_KEYS, ...DESIGNER_MATERIAL_KEYS },
}; };
// ─── Per-category accent colours ───────────────────────────────────────────── // ─── Per-category accent colours ─────────────────────────────────────────────
@ -398,6 +415,17 @@ const ACCENT = {
stripe: 'bg-cyan-600', stripe: 'bg-cyan-600',
badgeSkill: 'bg-cyan-900/40 border-cyan-700/50 text-cyan-300', badgeSkill: 'bg-cyan-900/40 border-cyan-700/50 text-cyan-300',
}, },
clothing: {
active: 'bg-violet-600 hover:bg-violet-700 text-white border-violet-600',
chip: 'bg-violet-600 text-white border-violet-600',
chipHover: 'hover:border-violet-500/60 hover:text-violet-400',
border: 'border-violet-500/40',
glow: 'shadow-violet-500/20',
icon: 'text-violet-400',
badge: 'bg-violet-500/15 text-violet-400 border-violet-500/30',
stripe: 'bg-violet-600',
badgeSkill: 'bg-violet-900/40 border-violet-700/50 text-violet-300',
},
}; };
// ─── FAB — offer services ───────────────────────────────────────────────────── // ─── FAB — offer services ─────────────────────────────────────────────────────
@ -433,6 +461,7 @@ function OfferFAB() {
{ to: '/become/judge', label: t('labUi.judgeEvents'), color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel }, { to: '/become/judge', label: t('labUi.judgeEvents'), color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel },
{ to: '/become/organizer', label: t('labUi.organizeEvents'), color: 'bg-teal-600 hover:bg-teal-700', icon: Tent }, { to: '/become/organizer', label: t('labUi.organizeEvents'), color: 'bg-teal-600 hover:bg-teal-700', icon: Tent },
{ to: '/become/barber', label: t('labUi.offerCuts'), color: 'bg-cyan-600 hover:bg-cyan-700', icon: Sparkles }, { to: '/become/barber', label: t('labUi.offerCuts'), color: 'bg-cyan-600 hover:bg-cyan-700', icon: Sparkles },
{ to: '/become/clothing', label: t('labUi.offerClothing'), color: 'bg-violet-600 hover:bg-violet-700', icon: Shirt },
]; ];
return ( return (
@ -530,6 +559,7 @@ function getSkillLabelForType(skill: string, serviceType: ServiceType, t: (key:
if (serviceType === 'judging') return JUDGE_ALL_LABELS[skill] || skill; if (serviceType === 'judging') return JUDGE_ALL_LABELS[skill] || skill;
if (serviceType === 'organizing') return ORGANIZER_ALL_LABELS[skill] || skill; if (serviceType === 'organizing') return ORGANIZER_ALL_LABELS[skill] || skill;
if (serviceType === 'barbering') return BARBER_ALL_LABELS[skill] || skill; if (serviceType === 'barbering') return BARBER_ALL_LABELS[skill] || skill;
if (serviceType === 'clothing') return DESIGNER_ALL_LABELS[skill] || skill;
return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill; return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill;
} }
@ -568,6 +598,7 @@ function LabToolbar({
{ key: 'judging', label: t('labUi.tabJudges'), icon: Gavel }, { key: 'judging', label: t('labUi.tabJudges'), icon: Gavel },
{ key: 'organizing', label: t('labUi.tabOrganizers'), icon: Tent }, { key: 'organizing', label: t('labUi.tabOrganizers'), icon: Tent },
{ key: 'barbering', label: t('labUi.tabBarbers'), icon: Sparkles }, { key: 'barbering', label: t('labUi.tabBarbers'), icon: Sparkles },
{ key: 'clothing', label: t('labUi.tabClothing'), icon: Shirt },
]; ];
const accent = ACCENT[serviceType]; const accent = ACCENT[serviceType];
@ -857,6 +888,12 @@ function FilterDrawer({
{ title: t('labUi.filterSignatureStyles'), entries: BARBER_STYLE_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' }, { title: t('labUi.filterSignatureStyles'), entries: BARBER_STYLE_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
{ title: t('labUi.filterSetting'), entries: BARBER_SETTING_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' }, { title: t('labUi.filterSetting'), entries: BARBER_SETTING_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
] ]
: serviceType === 'clothing'
? [
{ title: t('labUi.filterWhatTheyMake'), entries: DESIGNER_SERVICE_ENTRIES, activeColor: accent.chip },
{ title: t('labUi.filterMethod'), entries: DESIGNER_METHOD_ENTRIES, activeColor: 'bg-violet-400 text-white border-violet-400' },
{ title: t('labUi.filterMaterialsTrade'), entries: DESIGNER_MATERIAL_ENTRIES, activeColor: 'bg-violet-400 text-white border-violet-400' },
]
: [ : [
{ title: t('labUi.filterEditType'), entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip }, { title: t('labUi.filterEditType'), entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip },
{ title: t('labUi.filterSoftwareTools'), entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' }, { title: t('labUi.filterSoftwareTools'), entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' },
@ -1067,6 +1104,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music', judge: 'judging', artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music', judge: 'judging',
organizer: 'organizing', organizer: 'organizing',
barber: 'barbering', barber: 'barbering',
designer: 'clothing',
}; };
const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType]; const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType];
const accent = ACCENT[serviceType]; const accent = ACCENT[serviceType];
@ -1076,6 +1114,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music, judging: Gavel, art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music, judging: Gavel,
organizing: Tent, organizing: Tent,
barbering: Sparkles, barbering: Sparkles,
clothing: Shirt,
}; };
const TypeIcon = TYPE_ICONS[serviceType]; const TypeIcon = TYPE_ICONS[serviceType];
@ -1316,6 +1355,14 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
to: '/become/barber', to: '/become/barber',
color: 'bg-cyan-600 hover:bg-cyan-700', color: 'bg-cyan-600 hover:bg-cyan-700',
}, },
clothing: {
icon: Shirt,
title: t('labUi.empty.clothingTitle'),
body: t('labUi.empty.clothingBody'),
cta: t('labUi.offerClothing'),
to: '/become/clothing',
color: 'bg-violet-600 hover:bg-violet-700',
},
}; };
const c = configs[serviceType]; const c = configs[serviceType];
@ -1370,7 +1417,7 @@ export default function LabPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const initialCraft = (() => { const initialCraft = (() => {
const c = searchParams.get('craft'); const c = searchParams.get('craft');
const valid: ServiceType[] = ['coaching', 'filming', 'editing', 'photography', 'building', 'art', 'graffiti', 'tattoo', 'training', 'music', 'judging', 'organizing', 'barbering']; const valid: ServiceType[] = ['coaching', 'filming', 'editing', 'photography', 'building', 'art', 'graffiti', 'tattoo', 'training', 'music', 'judging', 'organizing', 'barbering', 'clothing'];
return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching'; return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching';
})(); })();
@ -1463,7 +1510,7 @@ export default function LabPage() {
const counts = useMemo<Record<ServiceType, number>>(() => { const counts = useMemo<Record<ServiceType, number>>(() => {
const empty: Record<ServiceType, number> = { const empty: Record<ServiceType, number> = {
coaching: 0, filming: 0, editing: 0, photography: 0, building: 0, coaching: 0, filming: 0, editing: 0, photography: 0, building: 0,
art: 0, graffiti: 0, tattoo: 0, training: 0, music: 0, judging: 0, organizing: 0, barbering: 0, art: 0, graffiti: 0, tattoo: 0, training: 0, music: 0, judging: 0, organizing: 0, barbering: 0, clothing: 0,
}; };
if (!profiles) return empty; if (!profiles) return empty;
const byType = (t: ServiceType) => const byType = (t: ServiceType) =>
@ -1482,6 +1529,7 @@ export default function LabPage() {
judging: byType('judging'), judging: byType('judging'),
organizing: byType('organizing'), organizing: byType('organizing'),
barbering: byType('barbering'), barbering: byType('barbering'),
clothing: byType('clothing'),
}; };
}, [profiles]); }, [profiles]);