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` |
|
| 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`
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ const ROUTES = [
|
||||||
'/lab',
|
'/lab',
|
||||||
'/become/organizer',
|
'/become/organizer',
|
||||||
'/become/barber',
|
'/become/barber',
|
||||||
|
'/become/clothing',
|
||||||
'/leaderboards',
|
'/leaderboards',
|
||||||
'/bounties',
|
'/bounties',
|
||||||
'/settings',
|
'/settings',
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue