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:
parent
f00031f5fa
commit
0b645d6d67
13 changed files with 387 additions and 13 deletions
2
NIP.md
2
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`
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ const ROUTES = [
|
|||
'/lab',
|
||||
'/become/organizer',
|
||||
'/become/barber',
|
||||
'/become/clothing',
|
||||
'/leaderboards',
|
||||
'/bounties',
|
||||
'/settings',
|
||||
|
|
|
|||
|
|
@ -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<CrewFocus, 'mixed'> | null {
|
||||
|
|
@ -24,6 +24,7 @@ function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed
|
|||
case 'art':
|
||||
case 'graffiti':
|
||||
case 'tattoo':
|
||||
case 'clothing':
|
||||
return 'art';
|
||||
case 'music':
|
||||
return 'music';
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import {
|
||||
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_BARBER_KIND,
|
||||
SKATE_BARBER_KIND, SKATE_DESIGNER_KIND,
|
||||
parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder,
|
||||
parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, parseSkateOrganizer,
|
||||
parseSkateBarber,
|
||||
parseSkateBarber, parseSkateDesigner,
|
||||
isFeaturedActive,
|
||||
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 SkateBarber,
|
||||
type SkateBarber, type SkateDesigner,
|
||||
} from '@/lib/types';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||
|
|
@ -18,12 +18,12 @@ import { verifiedFeaturedCoords } from '@/lib/featuredVerification';
|
|||
|
||||
export type LabProfileType =
|
||||
| '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 = (
|
||||
| SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder
|
||||
| SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge | SkateOrganizer
|
||||
| SkateBarber
|
||||
| SkateBarber | SkateDesigner
|
||||
) & {
|
||||
profileType: LabProfileType;
|
||||
};
|
||||
|
|
@ -31,13 +31,13 @@ export type LabProfile = (
|
|||
const LAB_KINDS = [
|
||||
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_BARBER_KIND,
|
||||
SKATE_BARBER_KIND, SKATE_DESIGNER_KIND,
|
||||
];
|
||||
|
||||
const LAB_SELF_TAGS = [
|
||||
'skatecoach', 'skatefilmer', 'skateeditor', 'skatephotographer', 'skatebuilder',
|
||||
'skateartist', 'skategraffiti', 'skatetattoo', 'skatetrainer', 'skatemusician', 'skatejudge', 'skateorganizer',
|
||||
'skatebarber',
|
||||
'skatebarber', 'skateclothing',
|
||||
];
|
||||
|
||||
/** 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,
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@ export const CRAFT_DETAIL_FIELDS: Partial<Record<LabProfileType, CraftDetailFiel
|
|||
barber: [
|
||||
{ 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: [
|
||||
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
|
||||
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },
|
||||
|
|
|
|||
|
|
@ -627,6 +627,47 @@ export const ARTIST_TOOL_KEYS: Record<string, string> = {
|
|||
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> = {
|
||||
throwups: 'labOptions.graffiti.style.throwups',
|
||||
pieces: 'labOptions.graffiti.style.pieces',
|
||||
|
|
|
|||
|
|
@ -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<number>([
|
|||
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<string, string> = {
|
|||
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 -----
|
||||
|
||||
// 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');
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
|||
...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
|
||||
const PHOTO_ALL_LABELS: Record<string, string> = {
|
||||
...PHOTO_SERVICE_LABELS,
|
||||
|
|
@ -216,7 +230,8 @@ const PHOTO_ALL_LABELS: Record<string, string> = {
|
|||
|
||||
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<ServiceType, LabProfile['profileType']> = {
|
||||
|
|
@ -233,6 +248,7 @@ const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> =
|
|||
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<ServiceType, Record<string, string>> = {
|
|||
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<Record<ServiceType, number>>(() => {
|
||||
const empty: Record<ServiceType, number> = {
|
||||
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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue