diff --git a/NIP.md b/NIP.md index 50ea6d8..623bc2e 100644 --- a/NIP.md +++ b/NIP.md @@ -1843,6 +1843,7 @@ They share a common base schema and are distinguished by kind and a self-tag. | 35012 | Event Judge | `skatejudge` | | 35013 | Event Organizer / Host | `skateorganizer` | | 35014 | Barber / Hairstylist | `skatebarber` | +| 35015 | Clothing Designer | `skateclothing` | > 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` - **Trainer**: `modalities` - **Barber**: `duration` +- **Clothing**: `minimum`, `turnaround`, `sizing`, `samples`, `customization` - **Filmer**: `turnaround`, `deliverables`, `drone` - **Editor**: `turnaround`, `deliverables` - **Photographer**: `turnaround`, `deliverables` diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 9ae404a..1632ecb 100755 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -134,6 +134,7 @@ const ROUTES = [ '/lab', '/become/organizer', '/become/barber', + '/become/clothing', '/leaderboards', '/bounties', '/settings', diff --git a/src/components/LabCrewsStrip.tsx b/src/components/LabCrewsStrip.tsx index 6e5d108..c3a5351 100644 --- a/src/components/LabCrewsStrip.tsx +++ b/src/components/LabCrewsStrip.tsx @@ -10,7 +10,7 @@ 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'; + | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering' | 'clothing'; /** Translate the Lab page's ServiceType into a crew focus scene (or null). */ function focusForLabService(serviceType: ServiceType): Exclude | null { @@ -24,6 +24,7 @@ function focusForLabService(serviceType: ServiceType): Exclude = { photographer: SKATE_PHOTOGRAPHER_KIND, builder: SKATE_BUILDER_KIND, artist: SKATE_ARTIST_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, - 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. */ @@ -104,6 +104,10 @@ export function parseLabProfile(event: NostrEvent): LabProfile | null { const p = parseSkateBarber(event); return p ? { ...p, profileType: 'barber' } : null; } + case SKATE_DESIGNER_KIND: { + const p = parseSkateDesigner(event); + return p ? { ...p, profileType: 'designer' } : null; + } default: return null; } diff --git a/src/i18n/batch4/labOptions.ts b/src/i18n/batch4/labOptions.ts index 1bc6272..2e59aa3 100644 --- a/src/i18n/batch4/labOptions.ts +++ b/src/i18n/batch4/labOptions.ts @@ -554,6 +554,60 @@ const labOptions = { 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: { skill: { beginner: { label: 'Beginner', description: 'New to skating or basic tricks' }, @@ -1110,6 +1164,60 @@ const labOptions = { 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: { skill: { beginner: { label: 'Principiante', description: 'Nuevo en el skate o trucos básicos' }, diff --git a/src/i18n/en.ts b/src/i18n/en.ts index e92491e..cec97ea 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2113,6 +2113,7 @@ labUi: { offer: 'Offer', offerBuilding: 'Offer Building', offerCoaching: 'Offer Coaching', + offerClothing: 'Offer Clothing', offerCuts: 'Offer Cuts', offerDesignArt: 'Offer Design / Art', offerEditing: 'Offer Editing', @@ -2183,6 +2184,7 @@ labUi: { tabJudges: 'Judges', tabOrganizers: 'Organizers', tabBarbers: 'Barbers', + tabClothing: 'Clothing', filterSkillsToLearn: 'Skills to Learn', filterContentType: 'Content Type', 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.', barbersTitle: 'No barbers yet', 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: { addNip05: 'Add NIP-05 in Settings', @@ -2895,6 +2899,9 @@ labUi: { drone: 'Drone', leadTime: 'Lead Time', warranty: 'Warranty', + sizing: 'Size Range', + samples: 'Samples / Prototypes', + customization: 'Customisation', }, craftForm: { profileImage: 'Profile image', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 1535d1e..f84ebb1 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -2113,6 +2113,7 @@ labUi: { offer: 'Ofrecer', offerBuilding: 'Ofrecer construcción', offerCoaching: 'Ofrecer clases', + offerClothing: 'Ofrecer ropa', offerCuts: 'Ofrecer cortes', offerDesignArt: 'Ofrecer diseño / arte', offerEditing: 'Ofrecer edición', @@ -2183,6 +2184,7 @@ labUi: { tabJudges: 'Jueces', tabOrganizers: 'Organizadores', tabBarbers: 'Barberos', + tabClothing: 'Ropa', filterSkillsToLearn: 'Trucos que enseñar', filterContentType: 'Tipo de contenido', 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.', 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.', + clothingTitle: 'Aún no hay diseñadores de ropa', + clothingBody: 'Sé el primero en ofrecer ropa personalizada, equipaciones y prendas para la escena.', }, become: { addNip05: 'Añade NIP-05 en Ajustes', @@ -2895,6 +2899,9 @@ labUi: { drone: 'Dron', leadTime: 'Plazo de entrega', warranty: 'Garantía', + sizing: 'Tallas', + samples: 'Muestras / prototipos', + customization: 'Personalización', }, craftForm: { profileImage: 'Imagen de perfil', diff --git a/src/i18n/legacy/becomeRoles.ts b/src/i18n/legacy/becomeRoles.ts index 4824b65..7c1af45 100644 --- a/src/i18n/legacy/becomeRoles.ts +++ b/src/i18n/legacy/becomeRoles.ts @@ -149,6 +149,23 @@ const becomeRolesUi = { booksOpenHelper: 'Are you currently taking bookings?', 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: { artist: { @@ -290,6 +307,23 @@ const becomeRolesUi = { booksOpenHelper: '¿Estás aceptando reservas ahora mismo?', 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', + }, }, }; diff --git a/src/lib/becomeRoles.tsx b/src/lib/becomeRoles.tsx index e83dd03..6d0aaea 100644 --- a/src/lib/becomeRoles.tsx +++ b/src/lib/becomeRoles.tsx @@ -1,6 +1,6 @@ import { lazy, type ComponentType, type LazyExoticComponent } from 'react'; import { - Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent, Sparkles, + Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent, Sparkles, Shirt, } from 'lucide-react'; import { pickRateUnits } from '@/lib/labExtras'; import type { CraftFormConfig } from '@/components/CraftProfileForm'; @@ -13,6 +13,7 @@ import { SKATE_TATTOO_KIND, TATTOO_STYLE_LABELS, TATTOO_METHOD_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_DESIGNER_KIND, DESIGNER_SERVICE_LABELS, DESIGNER_METHOD_LABELS, DESIGNER_MATERIAL_LABELS, } from '@/lib/types'; import { ARTIST_SERVICE_KEYS, ARTIST_MEDIUM_KEYS, ARTIST_TOOL_KEYS, @@ -23,6 +24,7 @@ import { TATTOO_STYLE_KEYS, TATTOO_METHOD_KEYS, TRAINER_FOCUS_KEYS, TRAINER_INJURY_KEYS, TRAINER_CREDENTIAL_KEYS, BARBER_SERVICE_KEYS, BARBER_STYLE_KEYS, BARBER_SETTING_KEYS, + DESIGNER_SERVICE_KEYS, DESIGNER_METHOD_KEYS, DESIGNER_MATERIAL_KEYS, } from '@/lib/labelKeys'; /** @@ -375,6 +377,46 @@ export const BECOME_ROLES: Record = { ], }, }, + 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 { diff --git a/src/lib/craftFields.ts b/src/lib/craftFields.ts index 21ced44..640fd69 100644 --- a/src/lib/craftFields.ts +++ b/src/lib/craftFields.ts @@ -44,6 +44,13 @@ export const CRAFT_DETAIL_FIELDS: Partial = { clipstudio: 'labOptions.artist.tool.clipstudio', }; +export const DESIGNER_SERVICE_KEYS: Record = { + 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 = { + 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 = { + 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 = { throwups: 'labOptions.graffiti.style.throwups', pieces: 'labOptions.graffiti.style.pieces', diff --git a/src/lib/types.ts b/src/lib/types.ts index 154817e..dd858bb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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_ORGANIZER_KIND = 35013; // Event organizers / hosts (jams, comps, meetups) 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 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 @@ -488,6 +489,7 @@ const COLLECTIVE_CAPABLE_KINDS = new Set([ 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_DESIGNER_KIND, ]); export function parseCollectiveJoinRequest(event: NostrEvent): CollectiveJoinRequest | null { @@ -1699,6 +1701,67 @@ export const ARTIST_TOOL_LABELS: Record = { clipstudio: 'Clip Studio', }; +// ----- Clothing designer (custom clothes, team kits, apparel) ----- + +// What they make +export const DESIGNER_SERVICE_LABELS: Record = { + 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 = { + 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 = { + 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 = { + 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 ----- // Styles @@ -2415,6 +2478,8 @@ export interface SkateArtist { event: NostrEvent; } +export type SkateDesigner = SkateArtist; + export interface SkateGraffiti { id: string; pubkey: string; @@ -3065,6 +3130,13 @@ export function parseSkateArtist(event: NostrEvent): SkateArtist | null { 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 { if (event.kind !== SKATE_GRAFFITI_KIND) return null; const base = parseCraftBase(event, 'skategraffiti'); diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx index 1f0dea5..bda5e5f 100644 --- a/src/pages/LabPage.tsx +++ b/src/pages/LabPage.tsx @@ -52,6 +52,9 @@ import { BARBER_SERVICE_LABELS, BARBER_STYLE_LABELS, BARBER_SETTING_LABELS, + DESIGNER_SERVICE_LABELS, + DESIGNER_METHOD_LABELS, + DESIGNER_MATERIAL_LABELS, } from '@/lib/types'; import { COACH_SKILL_KEYS, @@ -90,6 +93,9 @@ import { BARBER_SERVICE_KEYS, BARBER_STYLE_KEYS, BARBER_SETTING_KEYS, + DESIGNER_SERVICE_KEYS, + DESIGNER_METHOD_KEYS, + DESIGNER_MATERIAL_KEYS, } from '@/lib/labelKeys'; import { GraduationCap, @@ -120,6 +126,7 @@ import { Tent, Sparkles, Pencil, + Shirt, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { EmptyState as SharedEmptyState } from '@/components/EmptyState'; @@ -204,6 +211,13 @@ const BARBER_ALL_LABELS: Record = { ...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 = { + ...DESIGNER_SERVICE_LABELS, ...DESIGNER_METHOD_LABELS, ...DESIGNER_MATERIAL_LABELS, +}; + // All photographer label maps merged — for resolving a chip label regardless of category const PHOTO_ALL_LABELS: Record = { ...PHOTO_SERVICE_LABELS, @@ -216,7 +230,8 @@ const PHOTO_ALL_LABELS: Record = { type ServiceType = | '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. */ const SERVICE_TO_PROFILE_TYPE: Record = { @@ -233,6 +248,7 @@ const SERVICE_TO_PROFILE_TYPE: Record = judging: 'judge', organizing: 'organizer', barbering: 'barber', + clothing: 'designer', }; /** Lab option translation keys per service type (merged across each craft's maps). */ @@ -250,6 +266,7 @@ const KEYS_BY_TYPE: Record> = { judging: { ...JUDGE_FORMAT_KEYS, ...JUDGE_SPECIALTY_KEYS, ...JUDGE_LEVEL_KEYS }, organizing: { ...ORGANIZER_EVENT_KEYS, ...ORGANIZER_SERVICE_KEYS, ...ORGANIZER_SCALE_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 ───────────────────────────────────────────── @@ -398,6 +415,17 @@ const ACCENT = { stripe: 'bg-cyan-600', 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 ───────────────────────────────────────────────────── @@ -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/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/clothing', label: t('labUi.offerClothing'), color: 'bg-violet-600 hover:bg-violet-700', icon: Shirt }, ]; return ( @@ -530,6 +559,7 @@ function getSkillLabelForType(skill: string, serviceType: ServiceType, t: (key: if (serviceType === 'judging') return JUDGE_ALL_LABELS[skill] || skill; if (serviceType === 'organizing') return ORGANIZER_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; } @@ -568,6 +598,7 @@ function LabToolbar({ { key: 'judging', label: t('labUi.tabJudges'), icon: Gavel }, { key: 'organizing', label: t('labUi.tabOrganizers'), icon: Tent }, { key: 'barbering', label: t('labUi.tabBarbers'), icon: Sparkles }, + { key: 'clothing', label: t('labUi.tabClothing'), icon: Shirt }, ]; 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.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.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', organizer: 'organizing', barber: 'barbering', + designer: 'clothing', }; const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType]; 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, organizing: Tent, barbering: Sparkles, + clothing: Shirt, }; const TypeIcon = TYPE_ICONS[serviceType]; @@ -1316,6 +1355,14 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo to: '/become/barber', 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]; @@ -1370,7 +1417,7 @@ export default function LabPage() { const [searchParams] = useSearchParams(); const initialCraft = (() => { 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'; })(); @@ -1463,7 +1510,7 @@ export default function LabPage() { const counts = useMemo>(() => { const empty: Record = { 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; const byType = (t: ServiceType) => @@ -1482,6 +1529,7 @@ export default function LabPage() { judging: byType('judging'), organizing: byType('organizing'), barbering: byType('barbering'), + clothing: byType('clothing'), }; }, [profiles]);