feat(lab): add Barber / Hairstylist profile (kind 35014)
A full Lab role, mirroring how Event Organizer was added: - kinds: SKATE_BARBER_KIND 35014, self-tag 'skatebarber', BARBER_*_LABELS, SkateBarber + parseSkateBarber (studio, books_open). - craft config at /become/barber (cyan accent, Sparkles icon), Lab tab 'Barbers', filter sections (services/styles/setting), empty state, FAB item. - Lab pipeline: useLabProfiles kind/tag/parser, LabPage ServiceType 'barbering' + ACCENT/maps/counts, CoachDetailPage per-role maps + studio & books-open block, LabLinkField label, growth stats + Community Stats card. - No crew-scene strip for barbers (LabCrewsStrip returns null). - Documented in NIP.md; smoke test now boots /become/barber.
This commit is contained in:
parent
b70b9c677c
commit
a881a87f4d
12 changed files with 242 additions and 15 deletions
|
|
@ -44,7 +44,7 @@ session with **no other memory of this project**. So:
|
|||
· `31873` trusted-trader recommendation
|
||||
→ `src/hooks/useTradeAttestations.ts` / `useTraderRecommendations.ts`;
|
||||
reputation UI in `TradeReputationCard` / `TradeRecommendationsCard`
|
||||
- `35000–35013` role profiles (skater/filmer/coach/…/event-organizer) · `35006` crews · `30003` spot collections
|
||||
- `35000–35014` role profiles (skater/filmer/coach/…/event-organizer/barber) · `35006` crews · `30003` spot collections
|
||||
- **Lab offer flows** (`/become/*`): the 7 craft roles share
|
||||
`CraftProfileForm`; coach/filmer/editor/photographer/builder are bespoke but
|
||||
follow the same conventions. Forms **prefill from the existing profile** to
|
||||
|
|
|
|||
28
NIP.md
28
NIP.md
|
|
@ -1686,6 +1686,7 @@ They share a common base schema and are distinguished by kind and a self-tag.
|
|||
| 35011 | Musician / Band | `skatemusician` |
|
||||
| 35012 | Event Judge | `skatejudge` |
|
||||
| 35013 | Event Organizer / Host | `skateorganizer` |
|
||||
| 35014 | Barber / Hairstylist | `skatebarber` |
|
||||
|
||||
> Kind `35006` is **not** a Lab provider — it is the Skate Crew (see below).
|
||||
|
||||
|
|
@ -2132,12 +2133,35 @@ Organizer-specific optional tags:
|
|||
An organizing team can be a `collective` with member `p` tags, and may link to its
|
||||
skate crew via an `a` tag (`35006:pubkey:d`).
|
||||
|
||||
### Barber / Hairstylist (kind 35014)
|
||||
|
||||
Barbers and hairstylists who cut for the skate scene — shop chairs, mobile /
|
||||
house calls, and cuts at events or comps. They list the services they offer,
|
||||
their signature styles, and where they work, so skaters can find a fresh trim.
|
||||
Uses the common base schema.
|
||||
|
||||
Categories encoded as `t` tags:
|
||||
|
||||
- **Services**: `haircut`, `fade`, `beard`, `lineup`, `colour`, `kids`
|
||||
- **Signature styles**: `skin_fade`, `taper`, `buzz`, `mohawk`, `mullet`, `curly`, `long`, `classic`
|
||||
- **Setting**: `shop`, `mobile`, `event`
|
||||
|
||||
Barber-specific optional tags:
|
||||
|
||||
| Tag | Description |
|
||||
|-----|-------------|
|
||||
| `studio` | Shop / studio name (≤ `MAX_LOCATION_LEN`). |
|
||||
| `books_open` | `"true"` when currently taking bookings. |
|
||||
|
||||
A shop or collective can be a `collective` with member `p` tags, and may link to
|
||||
its skate crew via an `a` tag (`35006:pubkey:d`).
|
||||
|
||||
### Querying Lab Profiles
|
||||
|
||||
```json
|
||||
{
|
||||
"kinds": [35001, 35002, 35003, 35004, 35005, 35007, 35008, 35009, 35010, 35011, 35012, 35013],
|
||||
"#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer", "skatebuilder", "skateartist", "skategraffiti", "skatetattoo", "skatetrainer", "skatemusician", "skatejudge", "skateorganizer"],
|
||||
"kinds": [35001, 35002, 35003, 35004, 35005, 35007, 35008, 35009, 35010, 35011, 35012, 35013, 35014],
|
||||
"#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer", "skatebuilder", "skateartist", "skategraffiti", "skatetattoo", "skatetrainer", "skatemusician", "skatejudge", "skateorganizer", "skatebarber"],
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ const ROUTES = [
|
|||
'/trades',
|
||||
'/lab',
|
||||
'/become/organizer',
|
||||
'/become/barber',
|
||||
'/leaderboards',
|
||||
'/bounties',
|
||||
'/settings',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,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';
|
||||
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering';
|
||||
|
||||
/** Translate the Lab page's ServiceType into a crew focus scene (or null). */
|
||||
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> | null {
|
||||
|
|
@ -17,6 +17,7 @@ function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed
|
|||
// to fall through to "Photo & Film" and imply a wrong association.
|
||||
case 'judging':
|
||||
case 'organizing':
|
||||
case 'barbering':
|
||||
return null;
|
||||
case 'art':
|
||||
case 'graffiti':
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const TYPE_LABEL: Record<string, string> = {
|
|||
trainer: 'Trainer',
|
||||
musician: 'Musician',
|
||||
judge: 'Judge',
|
||||
barber: 'Barber',
|
||||
};
|
||||
|
||||
interface LabLinkFieldProps {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
SKATE_MUSICIAN_KIND,
|
||||
SKATE_JUDGE_KIND,
|
||||
SKATE_ORGANIZER_KIND,
|
||||
SKATE_BARBER_KIND,
|
||||
SKATE_TOUR_KIND,
|
||||
SKATE_CREW_KIND,
|
||||
} from '@/lib/types';
|
||||
|
|
@ -66,6 +67,7 @@ export interface GrowthStats {
|
|||
totalMusicians: number;
|
||||
totalJudges: number;
|
||||
totalOrganizers: number;
|
||||
totalBarbers: number;
|
||||
totalTours: number;
|
||||
totalCrews: number;
|
||||
/** All unique pubkeys that have published any content */
|
||||
|
|
@ -152,6 +154,7 @@ export function useGrowthStats() {
|
|||
musicianEvents,
|
||||
judgeEvents,
|
||||
organizerEvents,
|
||||
barberEvents,
|
||||
] = await Promise.all([
|
||||
querySkateContent([{ kinds: [SKATE_SPOT_KIND], '#t': ['skatespot'], limit: 2000 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_SHOP_KIND], limit: 2000 }], { signal }).catch(() => []),
|
||||
|
|
@ -172,6 +175,7 @@ export function useGrowthStats() {
|
|||
querySkateContent([{ kinds: [SKATE_MUSICIAN_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_JUDGE_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_ORGANIZER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_BARBER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
]);
|
||||
|
||||
// Deduplicate addressable events (keep newest per pubkey+kind+d)
|
||||
|
|
@ -203,8 +207,9 @@ export function useGrowthStats() {
|
|||
const musicians = dedup(musicianEvents);
|
||||
const judges = dedup(judgeEvents);
|
||||
const organizers = dedup(organizerEvents);
|
||||
const barbers = dedup(barberEvents);
|
||||
// Aggregate of the "other" crafts for active-mapper / total rollups.
|
||||
const otherLab = [...artists, ...graffiti, ...tattoo, ...trainers, ...musicians, ...judges, ...organizers];
|
||||
const otherLab = [...artists, ...graffiti, ...tattoo, ...trainers, ...musicians, ...judges, ...organizers, ...barbers];
|
||||
|
||||
// ── Monthly bucketing ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -326,6 +331,7 @@ export function useGrowthStats() {
|
|||
totalMusicians: musicians.length,
|
||||
totalJudges: judges.length,
|
||||
totalOrganizers: organizers.length,
|
||||
totalBarbers: barbers.length,
|
||||
totalTours: tours.length,
|
||||
totalCrews: crews.length,
|
||||
totalMappers: allPubkeys.size,
|
||||
|
|
|
|||
|
|
@ -2,11 +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,
|
||||
parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder,
|
||||
parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, parseSkateOrganizer,
|
||||
parseSkateBarber,
|
||||
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,
|
||||
} from '@/lib/types';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||
|
|
@ -14,11 +17,12 @@ import { useAdminModeration, filterAdminHiddenMixed } from '@/hooks/useAdminMode
|
|||
|
||||
export type LabProfileType =
|
||||
| 'coach' | 'filmer' | 'editor' | 'photographer' | 'builder'
|
||||
| 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge' | 'organizer';
|
||||
| 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge' | 'organizer' | 'barber';
|
||||
|
||||
export type LabProfile = (
|
||||
| SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder
|
||||
| SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge | SkateOrganizer
|
||||
| SkateBarber
|
||||
) & {
|
||||
profileType: LabProfileType;
|
||||
};
|
||||
|
|
@ -26,11 +30,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,
|
||||
];
|
||||
|
||||
const LAB_SELF_TAGS = [
|
||||
'skatecoach', 'skatefilmer', 'skateeditor', 'skatephotographer', 'skatebuilder',
|
||||
'skateartist', 'skategraffiti', 'skatetattoo', 'skatetrainer', 'skatemusician', 'skatejudge', 'skateorganizer',
|
||||
'skatebarber',
|
||||
];
|
||||
|
||||
/** Parse any Lab event into a LabProfile, or null if invalid / unknown kind. */
|
||||
|
|
@ -84,6 +90,10 @@ export function parseLabProfile(event: NostrEvent): LabProfile | null {
|
|||
const p = parseSkateOrganizer(event);
|
||||
return p ? { ...p, profileType: 'organizer' } : null;
|
||||
}
|
||||
case SKATE_BARBER_KIND: {
|
||||
const p = parseSkateBarber(event);
|
||||
return p ? { ...p, profileType: 'barber' } : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { lazy, type ComponentType, type LazyExoticComponent } from 'react';
|
||||
import {
|
||||
Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent,
|
||||
Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent, Sparkles,
|
||||
} from 'lucide-react';
|
||||
import type { CraftFormConfig } from '@/components/CraftProfileForm';
|
||||
import {
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
SKATE_ORGANIZER_KIND, ORGANIZER_EVENT_LABELS, ORGANIZER_SERVICE_LABELS, ORGANIZER_SCALE_LABELS,
|
||||
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,
|
||||
} from '@/lib/types';
|
||||
|
||||
/**
|
||||
|
|
@ -251,6 +252,35 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
|
|||
],
|
||||
},
|
||||
},
|
||||
barber: {
|
||||
kind: 'craft',
|
||||
seoTitle: 'Become a Barber / Hairstylist - SkateSpots',
|
||||
seoDescription: 'List your barbershop or chair for the skate scene — cuts, fades, beard trims, colour and more. Get booked by skaters.',
|
||||
config: {
|
||||
kind: SKATE_BARBER_KIND,
|
||||
selfTag: 'skatebarber',
|
||||
identifier: 'barber-profile',
|
||||
heading: 'Barber / Hairstylist',
|
||||
tagline: 'Cuts, fades & beard trims for the scene — shop chair or mobile',
|
||||
noun: 'barber',
|
||||
accentText: 'text-cyan-400',
|
||||
accentBg: 'bg-cyan-600',
|
||||
accentChip: 'bg-cyan-600 text-white border-cyan-600',
|
||||
accentChipHover: 'hover:border-cyan-500/60 hover:text-cyan-400',
|
||||
icon: Sparkles,
|
||||
rateLabel: 'Price / Cut',
|
||||
portfolioLabel: 'Cuts gallery / IG link',
|
||||
skillSections: [
|
||||
{ title: 'What You Offer', entries: Object.entries(BARBER_SERVICE_LABELS), required: true },
|
||||
{ title: 'Signature Styles', entries: Object.entries(BARBER_STYLE_LABELS) },
|
||||
{ title: 'Setting', entries: Object.entries(BARBER_SETTING_LABELS) },
|
||||
],
|
||||
extraFields: [
|
||||
{ tag: 'studio', label: 'Shop / Studio', placeholder: 'Shop or studio name (optional)' },
|
||||
{ tag: 'books_open', label: 'Books Open', type: 'toggle', helper: 'Are you currently taking bookings?' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getBecomeRole(slug: string | undefined): BecomeRole | undefined {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const SKATE_TRAINER_KIND = 35010; // Physical trainer / physiotherapist p
|
|||
export const SKATE_MUSICIAN_KIND = 35011; // Musician / band profiles (skaters who make music)
|
||||
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 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
|
||||
|
|
@ -1910,6 +1911,46 @@ export const ORGANIZER_SCALE_LABELS: Record<string, string> = {
|
|||
pro: 'Pro Circuit',
|
||||
};
|
||||
|
||||
// ----- Barbers / Hairstylists (kind 35014) -----
|
||||
|
||||
// What the barber offers.
|
||||
export const BARBER_SERVICE_LABELS: Record<string, string> = {
|
||||
haircut: 'Haircut',
|
||||
fade: 'Fade',
|
||||
beard: 'Beard Trim',
|
||||
lineup: 'Line-up / Shape-up',
|
||||
colour: 'Colour & Dye',
|
||||
kids: "Kids' Cuts",
|
||||
};
|
||||
|
||||
export const BARBER_SERVICE_DESCRIPTIONS: Record<string, string> = {
|
||||
haircut: 'A classic cut — clippers and scissors.',
|
||||
fade: 'A graduated fade — low, mid or high.',
|
||||
beard: 'Beard trim, shape and tidy-up.',
|
||||
lineup: 'Clean edges around the hairline and neck.',
|
||||
colour: 'Colour, bleach, dye and toning.',
|
||||
kids: 'Patient cuts for younger skaters.',
|
||||
};
|
||||
|
||||
// Signature styles.
|
||||
export const BARBER_STYLE_LABELS: Record<string, string> = {
|
||||
skin_fade: 'Skin Fade',
|
||||
taper: 'Taper',
|
||||
buzz: 'Buzz Cut',
|
||||
mohawk: 'Mohawk',
|
||||
mullet: 'Mullet',
|
||||
curly: 'Curls / Afro',
|
||||
long: 'Long Hair / Layers',
|
||||
classic: 'Classic Scissor Cut',
|
||||
};
|
||||
|
||||
// Where they cut hair.
|
||||
export const BARBER_SETTING_LABELS: Record<string, string> = {
|
||||
shop: 'Shop / Barbershop',
|
||||
mobile: 'Mobile / House Calls',
|
||||
event: 'Skate Events & Comps',
|
||||
};
|
||||
|
||||
// ----- Crews (community identity, map-anchored) -----
|
||||
|
||||
/**
|
||||
|
|
@ -3704,3 +3745,42 @@ export function parseSkateOrganizer(event: NostrEvent): SkateOrganizer | null {
|
|||
eventsHosted: event.tags.find(([name]) => name === 'events_hosted')?.[1],
|
||||
};
|
||||
}
|
||||
|
||||
export interface SkateBarber {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number;
|
||||
title: string; // barber / shop name
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
location?: string;
|
||||
image?: string;
|
||||
skills: string[]; // services + styles + settings tags
|
||||
experience?: string; // years cutting
|
||||
rate?: string; // price / cut
|
||||
phone?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
portfolio?: string; // Instagram / gallery of cuts
|
||||
social?: string;
|
||||
studio?: string; // shop / studio name
|
||||
booksOpen?: boolean; // currently taking bookings
|
||||
noffer?: string;
|
||||
entityType: EntityType;
|
||||
members: string[];
|
||||
crewRef?: string;
|
||||
createdAt: number;
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
export function parseSkateBarber(event: NostrEvent): SkateBarber | null {
|
||||
if (event.kind !== SKATE_BARBER_KIND) return null;
|
||||
const base = parseCraftBase(event, 'skatebarber');
|
||||
if (!base) return null;
|
||||
return {
|
||||
...base,
|
||||
studio: clampStr(event.tags.find(([name]) => name === 'studio')?.[1], MAX_LOCATION_LEN),
|
||||
booksOpen: event.tags.some(([name, value]) => name === 'books_open' && value === 'true'),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useAuthor } from '@/hooks/useAuthor';
|
|||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
||||
import { isAdminPubkey } from '@/lib/admins';
|
||||
import { COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, PHOTO_SERVICE_LABELS, PHOTO_SERVICE_DESCRIPTIONS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SERVICE_DESCRIPTIONS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS, ARTIST_SERVICE_LABELS, ARTIST_SERVICE_DESCRIPTIONS, ARTIST_MEDIUM_LABELS, ARTIST_TOOL_LABELS, GRAFFITI_STYLE_LABELS, GRAFFITI_STYLE_DESCRIPTIONS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS, TATTOO_STYLE_LABELS, TATTOO_STYLE_DESCRIPTIONS, TATTOO_METHOD_LABELS, TRAINER_FOCUS_LABELS, TRAINER_FOCUS_DESCRIPTIONS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS, 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, ORGANIZER_EVENT_LABELS, ORGANIZER_SERVICE_LABELS, ORGANIZER_SCALE_LABELS } from '@/lib/types';
|
||||
import { COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, PHOTO_SERVICE_LABELS, PHOTO_SERVICE_DESCRIPTIONS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SERVICE_DESCRIPTIONS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS, ARTIST_SERVICE_LABELS, ARTIST_SERVICE_DESCRIPTIONS, ARTIST_MEDIUM_LABELS, ARTIST_TOOL_LABELS, GRAFFITI_STYLE_LABELS, GRAFFITI_STYLE_DESCRIPTIONS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS, TATTOO_STYLE_LABELS, TATTOO_STYLE_DESCRIPTIONS, TATTOO_METHOD_LABELS, TRAINER_FOCUS_LABELS, TRAINER_FOCUS_DESCRIPTIONS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS, 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, ORGANIZER_EVENT_LABELS, ORGANIZER_SERVICE_LABELS, ORGANIZER_SCALE_LABELS, BARBER_SERVICE_LABELS, BARBER_SERVICE_DESCRIPTIONS, BARBER_STYLE_LABELS, BARBER_SETTING_LABELS, SKATE_BARBER_KIND } from '@/lib/types';
|
||||
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
||||
import { OfficialBadge } from '@/components/OfficialBadge';
|
||||
import { ProBadge } from '@/components/ProBadge';
|
||||
|
|
@ -49,6 +49,7 @@ import {
|
|||
Music,
|
||||
Gavel,
|
||||
Tent,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
|
|
@ -105,6 +106,7 @@ export default function CoachDetailPage() {
|
|||
const isEditor = profile?.profileType === 'editor';
|
||||
const isJudge = profile?.profileType === 'judge';
|
||||
const isOrganizer = profile?.profileType === 'organizer';
|
||||
const isBarber = profile?.profileType === 'barber';
|
||||
|
||||
useSeoMeta({
|
||||
title: profile ? `${profile.title} - SkateSpots` : 'Profile - SkateSpots',
|
||||
|
|
@ -141,6 +143,7 @@ export default function CoachDetailPage() {
|
|||
musician: { text: 'text-sky-400', bg: 'bg-sky-500', border: 'border-sky-500/30', hover: 'hover:bg-sky-500/10 border-sky-500' },
|
||||
judge: { text: 'text-yellow-400', bg: 'bg-yellow-500', border: 'border-yellow-500/30', hover: 'hover:bg-yellow-500/10 border-yellow-500' },
|
||||
organizer: { text: 'text-teal-400', bg: 'bg-teal-600', border: 'border-teal-500/30', hover: 'hover:bg-teal-500/10 border-teal-500' },
|
||||
barber: { text: 'text-cyan-400', bg: 'bg-cyan-600', border: 'border-cyan-500/30', hover: 'hover:bg-cyan-500/10 border-cyan-500' },
|
||||
};
|
||||
const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach;
|
||||
|
||||
|
|
@ -160,6 +163,7 @@ export default function CoachDetailPage() {
|
|||
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
||||
artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel,
|
||||
organizer: Tent,
|
||||
barber: Sparkles,
|
||||
};
|
||||
const TypeIcon = TYPE_ICONS[profile.profileType] ?? GraduationCap;
|
||||
|
||||
|
|
@ -169,6 +173,7 @@ export default function CoachDetailPage() {
|
|||
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,
|
||||
};
|
||||
const profileKind = KINDS[profile.profileType] ?? SKATE_COACH_KIND;
|
||||
const isCollective = profile.entityType === 'collective' && profile.members.length > 0;
|
||||
|
|
@ -187,6 +192,7 @@ export default function CoachDetailPage() {
|
|||
musician: { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS },
|
||||
judge: { ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS },
|
||||
organizer: { ...ORGANIZER_EVENT_LABELS, ...ORGANIZER_SERVICE_LABELS, ...ORGANIZER_SCALE_LABELS },
|
||||
barber: { ...BARBER_SERVICE_LABELS, ...BARBER_STYLE_LABELS, ...BARBER_SETTING_LABELS },
|
||||
};
|
||||
const DESCRIPTIONS_BY_TYPE: Record<string, Record<string, string>> = {
|
||||
coach: COACH_SKILL_DESCRIPTIONS,
|
||||
|
|
@ -201,6 +207,7 @@ export default function CoachDetailPage() {
|
|||
musician: {},
|
||||
judge: {},
|
||||
organizer: {},
|
||||
barber: BARBER_SERVICE_DESCRIPTIONS,
|
||||
};
|
||||
|
||||
const getSkillLabel = (skill: string) => LABELS_BY_TYPE[profile.profileType]?.[skill] || skill;
|
||||
|
|
@ -210,6 +217,7 @@ export default function CoachDetailPage() {
|
|||
coach: 'coach', filmer: 'filmer', editor: 'video editor', photographer: 'photographer', builder: 'builder',
|
||||
artist: 'artist', graffiti: 'graffiti artist', tattoo: 'tattoo artist', trainer: 'trainer', musician: 'musician', judge: 'event judge',
|
||||
organizer: 'event organizer',
|
||||
barber: 'barber',
|
||||
};
|
||||
const typeNoun = TYPE_NOUNS[profile.profileType] ?? 'profile';
|
||||
|
||||
|
|
@ -217,6 +225,7 @@ export default function CoachDetailPage() {
|
|||
coach: 'Skills', photographer: 'Services & Gear', builder: 'What We Build',
|
||||
artist: 'What They Make', graffiti: 'Styles & Work', tattoo: 'Styles',
|
||||
trainer: 'Focus & Specialties', musician: 'Genres & Instruments', judge: 'Formats & Specialties',
|
||||
barber: 'Services & Styles', organizer: 'Event Types & Services',
|
||||
};
|
||||
const skillsTitle = SKILLS_TITLES[profile.profileType] ?? 'Services & Skills';
|
||||
|
||||
|
|
@ -435,6 +444,26 @@ export default function CoachDetailPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Barber — shop & books-open */}
|
||||
{isBarber && (pget('studio') || pbool('booksOpen')) && (
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{pget('studio') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border}`}>
|
||||
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1 flex items-center gap-1">
|
||||
<MapPin className={`h-3 w-3 ${accent.text}`} /> Shop / Studio
|
||||
</h4>
|
||||
<p className="text-sm text-gray-300">{pget('studio')}</p>
|
||||
</div>
|
||||
)}
|
||||
{pbool('booksOpen') && (
|
||||
<div className={`bg-black/50 border rounded-lg p-3 ${accent.border} flex items-center gap-2`}>
|
||||
<Sparkles className={`h-4 w-4 ${accent.text}`} />
|
||||
<span className="text-sm font-bold text-emerald-400">Books Open</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trainer — credentials & remote */}
|
||||
{isTrainer && (pget('credentials') || pbool('remote')) && (
|
||||
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { cn } from '@/lib/utils';
|
|||
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
|
||||
import { SKATE_RELAY } from '@/lib/types';
|
||||
import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts';
|
||||
import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, Trophy, FlaskConical, Palette, SprayCan, PenTool, Dumbbell, Music, Gavel, Tent } from 'lucide-react';
|
||||
import { MapPin, Store, Calendar, GraduationCap, Video, Scissors, Route, Users, Users2, Camera, Hammer, TrendingUp, Radio, Zap, ExternalLink, Trophy, FlaskConical, Palette, SprayCan, PenTool, Dumbbell, Music, Gavel, Tent, Sparkles } from 'lucide-react';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -22,12 +22,13 @@ function labCraftTotal(s: {
|
|||
totalCoaches: number; totalFilmers: number; totalEditors: number;
|
||||
totalPhotographers: number; totalBuilders: number; totalArtists: number;
|
||||
totalGraffiti: number; totalTattoo: number; totalTrainers: number;
|
||||
totalMusicians: number; totalJudges: number; totalOrganizers: number;
|
||||
totalMusicians: number; totalJudges: number; totalOrganizers: number; totalBarbers: number;
|
||||
}): number {
|
||||
return (
|
||||
s.totalCoaches + s.totalFilmers + s.totalEditors + s.totalPhotographers +
|
||||
s.totalBuilders + s.totalArtists + s.totalGraffiti + s.totalTattoo +
|
||||
s.totalTrainers + s.totalMusicians + s.totalJudges + s.totalOrganizers
|
||||
s.totalTrainers + s.totalMusicians + s.totalJudges + s.totalOrganizers +
|
||||
s.totalBarbers
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +262,8 @@ export default function CommunityStatsPage() {
|
|||
<StatCard icon={Dumbbell} value={stats?.totalTrainers ?? 0} label="Trainers" color="#34d399" loading={isLoading} to="/lab?craft=training" />
|
||||
<StatCard icon={Music} value={stats?.totalMusicians ?? 0} label="Musicians" color="#818cf8" loading={isLoading} to="/lab?craft=music" />
|
||||
<StatCard icon={Gavel} value={stats?.totalJudges ?? 0} label="Judges" color="#fbbf24" loading={isLoading} to="/lab?craft=judging" />
|
||||
<StatCard icon={Tent} value={stats?.totalOrganizers ?? 0} label="Organizers" color="#22d3ee" loading={isLoading} to="/lab?craft=organizing" />
|
||||
<StatCard icon={Tent} value={stats?.totalOrganizers ?? 0} label="Organizers" color="#2dd4bf" loading={isLoading} to="/lab?craft=organizing" />
|
||||
<StatCard icon={Sparkles} value={stats?.totalBarbers ?? 0} label="Barbers" color="#22d3ee" loading={isLoading} to="/lab?craft=barbering" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ import {
|
|||
ORGANIZER_EVENT_LABELS,
|
||||
ORGANIZER_SERVICE_LABELS,
|
||||
ORGANIZER_SCALE_LABELS,
|
||||
BARBER_SERVICE_LABELS,
|
||||
BARBER_STYLE_LABELS,
|
||||
BARBER_SETTING_LABELS,
|
||||
} from '@/lib/types';
|
||||
import {
|
||||
GraduationCap,
|
||||
|
|
@ -75,6 +78,7 @@ import {
|
|||
PersonStanding,
|
||||
ArrowRight,
|
||||
Tent,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
|
||||
|
|
@ -152,6 +156,13 @@ const ORGANIZER_ALL_LABELS: Record<string, string> = {
|
|||
...ORGANIZER_EVENT_LABELS, ...ORGANIZER_SERVICE_LABELS, ...ORGANIZER_SCALE_LABELS,
|
||||
};
|
||||
|
||||
const BARBER_SERVICE_ENTRIES = Object.entries(BARBER_SERVICE_LABELS);
|
||||
const BARBER_STYLE_ENTRIES = Object.entries(BARBER_STYLE_LABELS);
|
||||
const BARBER_SETTING_ENTRIES = Object.entries(BARBER_SETTING_LABELS);
|
||||
const BARBER_ALL_LABELS: Record<string, string> = {
|
||||
...BARBER_SERVICE_LABELS, ...BARBER_STYLE_LABELS, ...BARBER_SETTING_LABELS,
|
||||
};
|
||||
|
||||
// All photographer label maps merged — for resolving a chip label regardless of category
|
||||
const PHOTO_ALL_LABELS: Record<string, string> = {
|
||||
...PHOTO_SERVICE_LABELS,
|
||||
|
|
@ -164,7 +175,7 @@ const PHOTO_ALL_LABELS: Record<string, string> = {
|
|||
|
||||
type ServiceType =
|
||||
| 'coaching' | 'filming' | 'editing' | 'photography' | 'building'
|
||||
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing';
|
||||
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing' | 'barbering';
|
||||
|
||||
/** Map a ServiceType to the LabProfile.profileType it filters. */
|
||||
const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> = {
|
||||
|
|
@ -180,6 +191,7 @@ const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> =
|
|||
music: 'musician',
|
||||
judging: 'judge',
|
||||
organizing: 'organizer',
|
||||
barbering: 'barber',
|
||||
};
|
||||
|
||||
// ─── Per-category accent colours ─────────────────────────────────────────────
|
||||
|
|
@ -317,6 +329,17 @@ const ACCENT = {
|
|||
stripe: 'bg-teal-600',
|
||||
badgeSkill: 'bg-teal-900/40 border-teal-700/50 text-teal-300',
|
||||
},
|
||||
barbering: {
|
||||
active: 'bg-cyan-600 hover:bg-cyan-700 text-white border-cyan-600',
|
||||
chip: 'bg-cyan-600 text-white border-cyan-600',
|
||||
chipHover: 'hover:border-cyan-500/60 hover:text-cyan-400',
|
||||
border: 'border-cyan-500/40',
|
||||
glow: 'shadow-cyan-500/20',
|
||||
icon: 'text-cyan-400',
|
||||
badge: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/30',
|
||||
stripe: 'bg-cyan-600',
|
||||
badgeSkill: 'bg-cyan-900/40 border-cyan-700/50 text-cyan-300',
|
||||
},
|
||||
};
|
||||
|
||||
// ─── FAB — offer services ─────────────────────────────────────────────────────
|
||||
|
|
@ -348,6 +371,7 @@ function OfferFAB() {
|
|||
{ to: '/become/musician', label: 'Add Music Profile', color: 'bg-sky-500 hover:bg-sky-600', icon: Music },
|
||||
{ to: '/become/judge', label: 'Judge Events', color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel },
|
||||
{ to: '/become/organizer', label: 'Organize Events', color: 'bg-teal-600 hover:bg-teal-700', icon: Tent },
|
||||
{ to: '/become/barber', label: 'Offer Cuts', color: 'bg-cyan-600 hover:bg-cyan-700', icon: Sparkles },
|
||||
{ to: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard },
|
||||
];
|
||||
|
||||
|
|
@ -426,6 +450,7 @@ function getSkillLabelForType(skill: string, serviceType: ServiceType): string {
|
|||
if (serviceType === 'music') return MUSICIAN_ALL_LABELS[skill] || skill;
|
||||
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;
|
||||
return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill;
|
||||
}
|
||||
|
||||
|
|
@ -460,6 +485,7 @@ function LabToolbar({
|
|||
{ key: 'music', label: 'Music', icon: Music },
|
||||
{ key: 'judging', label: 'Judges', icon: Gavel },
|
||||
{ key: 'organizing', label: 'Organizers', icon: Tent },
|
||||
{ key: 'barbering', label: 'Barbers', icon: Sparkles },
|
||||
];
|
||||
|
||||
const accent = ACCENT[serviceType];
|
||||
|
|
@ -721,6 +747,12 @@ function FilterDrawer({
|
|||
{ title: 'What You Handle', entries: ORGANIZER_SERVICE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
|
||||
{ title: 'Scale', entries: ORGANIZER_SCALE_ENTRIES, activeColor: 'bg-teal-400 text-black border-teal-400' },
|
||||
]
|
||||
: serviceType === 'barbering'
|
||||
? [
|
||||
{ title: 'What They Offer', entries: BARBER_SERVICE_ENTRIES, activeColor: accent.chip },
|
||||
{ title: 'Signature Styles', entries: BARBER_STYLE_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
|
||||
{ title: 'Setting', entries: BARBER_SETTING_ENTRIES, activeColor: 'bg-cyan-400 text-white border-cyan-400' },
|
||||
]
|
||||
: [
|
||||
{ title: 'Edit Type', entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip },
|
||||
{ title: 'Software & Tools', entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' },
|
||||
|
|
@ -897,6 +929,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
|||
coach: 'coaching', filmer: 'filming', editor: 'editing', photographer: 'photography', builder: 'building',
|
||||
artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music', judge: 'judging',
|
||||
organizer: 'organizing',
|
||||
barber: 'barbering',
|
||||
};
|
||||
const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType];
|
||||
const accent = ACCENT[serviceType];
|
||||
|
|
@ -905,6 +938,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
|||
coaching: GraduationCap, filming: Video, editing: Scissors, photography: Camera, building: HardHat,
|
||||
art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music, judging: Gavel,
|
||||
organizing: Tent,
|
||||
barbering: Sparkles,
|
||||
};
|
||||
const TypeIcon = TYPE_ICONS[serviceType];
|
||||
|
||||
|
|
@ -1136,6 +1170,14 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
to: '/become/organizer',
|
||||
color: 'bg-teal-600 hover:bg-teal-700',
|
||||
},
|
||||
barbering: {
|
||||
icon: Sparkles,
|
||||
title: 'No barbers yet',
|
||||
body: 'Cut hair for skaters? List your chair or mobile service so the scene can get a fresh trim.',
|
||||
cta: 'Offer Cuts',
|
||||
to: '/become/barber',
|
||||
color: 'bg-cyan-600 hover:bg-cyan-700',
|
||||
},
|
||||
};
|
||||
|
||||
const c = configs[serviceType];
|
||||
|
|
@ -1186,7 +1228,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'];
|
||||
const valid: ServiceType[] = ['coaching', 'filming', 'editing', 'photography', 'building', 'art', 'graffiti', 'tattoo', 'training', 'music', 'judging', 'organizing', 'barbering'];
|
||||
return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching';
|
||||
})();
|
||||
|
||||
|
|
@ -1235,7 +1277,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,
|
||||
art: 0, graffiti: 0, tattoo: 0, training: 0, music: 0, judging: 0, organizing: 0, barbering: 0,
|
||||
};
|
||||
if (!profiles) return empty;
|
||||
const byType = (t: ServiceType) =>
|
||||
|
|
@ -1253,6 +1295,7 @@ export default function LabPage() {
|
|||
music: byType('music'),
|
||||
judging: byType('judging'),
|
||||
organizing: byType('organizing'),
|
||||
barbering: byType('barbering'),
|
||||
};
|
||||
}, [profiles]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue