feat(lab): add Event Organizer craft profile (kind 35013)
New data-driven Lab role 'Event Organizer / Host' — skaters who put on jams, comps, meetups, demos and tour stops. - types.ts: SKATE_ORGANIZER_KIND=35013, ORGANIZER_EVENT/SERVICE/SCALE label maps, SkateOrganizer + parseSkateOrganizer (events_hosted extra tag) - becomeRoles: craft config (teal accent, Tent icon, 3 skill sections) - useLabProfiles: registered kind/self-tag/parser - LabPage: Organizers tab, accent, filter drawer section, empty state, Offer link, counts - CoachDetailPage (generic Lab detail): accent/icon/kind/labels/noun - LabCrewsStrip: union updated - NIP.md documents kind 35013; smoke covers /lab and /become/organizer Gate: 101 tests, 0 errors/warnings, boot smoke green.
This commit is contained in:
parent
e1deec21bd
commit
b60b13dc18
9 changed files with 212 additions and 16 deletions
|
|
@ -44,7 +44,7 @@ session with **no other memory of this project**. So:
|
||||||
· `31873` trusted-trader recommendation
|
· `31873` trusted-trader recommendation
|
||||||
→ `src/hooks/useTradeAttestations.ts` / `useTraderRecommendations.ts`;
|
→ `src/hooks/useTradeAttestations.ts` / `useTraderRecommendations.ts`;
|
||||||
reputation UI in `TradeReputationCard` / `TradeRecommendationsCard`
|
reputation UI in `TradeReputationCard` / `TradeRecommendationsCard`
|
||||||
- `35000–35012` role profiles (skater/filmer/coach/…) · `35006` crews · `30003` spot collections
|
- `35000–35013` role profiles (skater/filmer/coach/…/event-organizer) · `35006` crews · `30003` spot collections
|
||||||
- `24899` spot chat · `2259` spot status · `32671` Sasquatch Pro · `7103` event updates
|
- `24899` spot chat · `2259` spot status · `32671` Sasquatch Pro · `7103` event updates
|
||||||
- **Marketplace**: `GearSwapPage` (grid) + `MarketplaceItemCard` and a listing
|
- **Marketplace**: `GearSwapPage` (grid) + `MarketplaceItemCard` and a listing
|
||||||
detail page at **`/listing/:pubkey/:identifier`**. Trade/escrow actions live in
|
detail page at **`/listing/:pubkey/:identifier`**. Trade/escrow actions live in
|
||||||
|
|
|
||||||
28
NIP.md
28
NIP.md
|
|
@ -1685,6 +1685,7 @@ They share a common base schema and are distinguished by kind and a self-tag.
|
||||||
| 35010 | Trainer / Physiotherapist | `skatetrainer` |
|
| 35010 | Trainer / Physiotherapist | `skatetrainer` |
|
||||||
| 35011 | Musician / Band | `skatemusician` |
|
| 35011 | Musician / Band | `skatemusician` |
|
||||||
| 35012 | Event Judge | `skatejudge` |
|
| 35012 | Event Judge | `skatejudge` |
|
||||||
|
| 35013 | Event Organizer / Host | `skateorganizer` |
|
||||||
|
|
||||||
> 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).
|
||||||
|
|
||||||
|
|
@ -2108,12 +2109,35 @@ Judge-specific optional tags:
|
||||||
A judging panel can be a `collective` with member `p` tags, and may link to its
|
A judging panel can be a `collective` with member `p` tags, and may link to its
|
||||||
skate crew via an `a` tag (`35006:pubkey:d`).
|
skate crew via an `a` tag (`35006:pubkey:d`).
|
||||||
|
|
||||||
|
### Event Organizer (kind 35013)
|
||||||
|
|
||||||
|
Skaters who put on events — street jams, best-trick comps, Game of S.K.A.T.E.,
|
||||||
|
meetups, demos, clinics and tour stops. Organizers list the event types they run,
|
||||||
|
what they take care of (permits, sponsorship, sound, safety…) and the scale they
|
||||||
|
work at, so skaters and brands can find and book them. Uses the common base
|
||||||
|
schema.
|
||||||
|
|
||||||
|
Categories encoded as `t` tags:
|
||||||
|
|
||||||
|
- **Event types**: `street_jam`, `best_trick`, `game_of_skate`, `contest`, `bowl`, `mini_ramp`, `park`, `demo`, `clinic`, `meetup`, `downhill`, `slalom`, `kids`, `charity`, `tour_stop`, `premiere`, `market`
|
||||||
|
- **What they handle**: `organizing`, `hosting`, `permits`, `sponsorship`, `promotion`, `livestream`, `sound`, `ramps`, `safety`, `vendors`, `media`, `registration`
|
||||||
|
- **Scale**: `grassroots`, `local`, `regional`, `national`, `international`, `pro`
|
||||||
|
|
||||||
|
Organizer-specific optional tags:
|
||||||
|
|
||||||
|
| Tag | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `events_hosted` | Freeform note on notable events / comps / meetups run (≤ base limits). |
|
||||||
|
|
||||||
|
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`).
|
||||||
|
|
||||||
### Querying Lab Profiles
|
### Querying Lab Profiles
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"kinds": [35001, 35002, 35003, 35004, 35005, 35007, 35008, 35009, 35010, 35011, 35012],
|
"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"],
|
"#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer", "skatebuilder", "skateartist", "skategraffiti", "skatetattoo", "skatetrainer", "skatemusician", "skatejudge", "skateorganizer"],
|
||||||
"limit": 100
|
"limit": 100
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,8 @@ const ROUTES = [
|
||||||
'/marketplace',
|
'/marketplace',
|
||||||
'/listing/abc/def',
|
'/listing/abc/def',
|
||||||
'/trades',
|
'/trades',
|
||||||
|
'/lab',
|
||||||
|
'/become/organizer',
|
||||||
'/leaderboards',
|
'/leaderboards',
|
||||||
'/bounties',
|
'/bounties',
|
||||||
'/settings',
|
'/settings',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,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';
|
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing';
|
||||||
|
|
||||||
/** Translate the Lab page's ServiceType into a crew focus scene. */
|
/** Translate the Lab page's ServiceType into a crew focus scene. */
|
||||||
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> {
|
function focusForLabService(serviceType: ServiceType): Exclude<CrewFocus, 'mixed'> {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_KIND,
|
||||||
parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder,
|
parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder,
|
||||||
parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge,
|
parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, parseSkateOrganizer,
|
||||||
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 SkateArtist, type SkateGraffiti, type SkateTattoo, type SkateTrainer, type SkateMusician, type SkateJudge, type SkateOrganizer,
|
||||||
} 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';
|
||||||
|
|
@ -14,23 +14,23 @@ import { useAdminModeration, filterAdminHiddenMixed } from '@/hooks/useAdminMode
|
||||||
|
|
||||||
export type LabProfileType =
|
export type LabProfileType =
|
||||||
| 'coach' | 'filmer' | 'editor' | 'photographer' | 'builder'
|
| 'coach' | 'filmer' | 'editor' | 'photographer' | 'builder'
|
||||||
| 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge';
|
| 'artist' | 'graffiti' | 'tattoo' | 'trainer' | 'musician' | 'judge' | 'organizer';
|
||||||
|
|
||||||
export type LabProfile = (
|
export type LabProfile = (
|
||||||
| SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder
|
| SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder
|
||||||
| SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge
|
| SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge | SkateOrganizer
|
||||||
) & {
|
) & {
|
||||||
profileType: LabProfileType;
|
profileType: LabProfileType;
|
||||||
};
|
};
|
||||||
|
|
||||||
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_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND, SKATE_JUDGE_KIND, SKATE_ORGANIZER_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',
|
'skateartist', 'skategraffiti', 'skatetattoo', 'skatetrainer', 'skatemusician', 'skatejudge', 'skateorganizer',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 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. */
|
||||||
|
|
@ -80,6 +80,10 @@ export function parseLabProfile(event: NostrEvent): LabProfile | null {
|
||||||
const p = parseSkateJudge(event);
|
const p = parseSkateJudge(event);
|
||||||
return p ? { ...p, profileType: 'judge' } : null;
|
return p ? { ...p, profileType: 'judge' } : null;
|
||||||
}
|
}
|
||||||
|
case SKATE_ORGANIZER_KIND: {
|
||||||
|
const p = parseSkateOrganizer(event);
|
||||||
|
return p ? { ...p, profileType: 'organizer' } : null;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
Palette, SprayCan, Music, Gavel, Brush, Dumbbell, Tent,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { CraftFormConfig } from '@/components/CraftProfileForm';
|
import type { CraftFormConfig } from '@/components/CraftProfileForm';
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SKATE_GRAFFITI_KIND, GRAFFITI_STYLE_LABELS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS,
|
SKATE_GRAFFITI_KIND, GRAFFITI_STYLE_LABELS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS,
|
||||||
SKATE_MUSICIAN_KIND, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS,
|
SKATE_MUSICIAN_KIND, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS,
|
||||||
SKATE_JUDGE_KIND, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS,
|
SKATE_JUDGE_KIND, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS,
|
||||||
|
SKATE_ORGANIZER_KIND, ORGANIZER_EVENT_LABELS, ORGANIZER_SERVICE_LABELS, ORGANIZER_SCALE_LABELS,
|
||||||
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,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
|
|
@ -165,6 +166,34 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
organizer: {
|
||||||
|
kind: 'craft',
|
||||||
|
seoTitle: 'Organize Skate Events - SkateSpots',
|
||||||
|
seoDescription: 'List yourself as a skate event organizer — street jams, best trick, contests, meetups, demos and tour stops. Get discovered and booked to run the session.',
|
||||||
|
config: {
|
||||||
|
kind: SKATE_ORGANIZER_KIND,
|
||||||
|
selfTag: 'skateorganizer',
|
||||||
|
identifier: 'organizer-profile',
|
||||||
|
heading: 'Event Organizer',
|
||||||
|
tagline: 'Jams, contests, meetups & demos — put on the session and bring the scene together',
|
||||||
|
noun: 'organizer',
|
||||||
|
accentText: 'text-teal-400',
|
||||||
|
accentBg: 'bg-teal-600',
|
||||||
|
accentChip: 'bg-teal-600 text-white border-teal-600',
|
||||||
|
accentChipHover: 'hover:border-teal-500/60 hover:text-teal-400',
|
||||||
|
icon: Tent,
|
||||||
|
rateLabel: 'Event Fee / Budget',
|
||||||
|
portfolioLabel: 'Past events / results link',
|
||||||
|
skillSections: [
|
||||||
|
{ title: 'Event Types', entries: Object.entries(ORGANIZER_EVENT_LABELS), required: true },
|
||||||
|
{ title: 'What You Handle', entries: Object.entries(ORGANIZER_SERVICE_LABELS) },
|
||||||
|
{ title: 'Scale', entries: Object.entries(ORGANIZER_SCALE_LABELS) },
|
||||||
|
],
|
||||||
|
extraFields: [
|
||||||
|
{ tag: 'events_hosted', label: 'Events Organized', placeholder: 'e.g. 25 jams, 3 city comps', helper: 'Notable events, comps or meetups you have run.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
tattoo: {
|
tattoo: {
|
||||||
kind: 'craft',
|
kind: 'craft',
|
||||||
seoTitle: 'Become a Tattoo Artist - SkateSpots',
|
seoTitle: 'Become a Tattoo Artist - SkateSpots',
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export const SKATE_TATTOO_KIND = 35009; // Tattoo artist profiles
|
||||||
export const SKATE_TRAINER_KIND = 35010; // Physical trainer / physiotherapist profiles
|
export const SKATE_TRAINER_KIND = 35010; // Physical trainer / physiotherapist profiles
|
||||||
export const SKATE_MUSICIAN_KIND = 35011; // Musician / band profiles (skaters who make music)
|
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_JUDGE_KIND = 35012; // Skate judges (event officials)
|
||||||
|
export const SKATE_ORGANIZER_KIND = 35013; // Event organizers / hosts (jams, comps, meetups)
|
||||||
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
|
||||||
|
|
@ -1857,6 +1858,55 @@ export const JUDGE_LEVEL_LABELS: Record<string, string> = {
|
||||||
pro: 'Pro Circuit',
|
pro: 'Pro Circuit',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----- Event Organizers / Hosts (kind 35013) -----
|
||||||
|
|
||||||
|
// The kinds of events the organizer puts on.
|
||||||
|
export const ORGANIZER_EVENT_LABELS: Record<string, string> = {
|
||||||
|
street_jam: 'Street Jam',
|
||||||
|
best_trick: 'Best Trick',
|
||||||
|
game_of_skate: 'Game of S.K.A.T.E.',
|
||||||
|
contest: 'Contest / Comp',
|
||||||
|
bowl: 'Bowl / Vert',
|
||||||
|
mini_ramp: 'Mini Ramp',
|
||||||
|
park: 'Park Session',
|
||||||
|
demo: 'Pro Demo',
|
||||||
|
clinic: 'Clinic / Workshop',
|
||||||
|
meetup: 'Meetup / Cruise',
|
||||||
|
downhill: 'Downhill / Race',
|
||||||
|
slalom: 'Slalom',
|
||||||
|
kids: 'Kids / Youth',
|
||||||
|
charity: 'Charity / Fundraiser',
|
||||||
|
tour_stop: 'Tour Stop',
|
||||||
|
premiere: 'Video Premiere',
|
||||||
|
market: 'Market / Art Show',
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the organizer actually takes care of.
|
||||||
|
export const ORGANIZER_SERVICE_LABELS: Record<string, string> = {
|
||||||
|
organizing: 'Full Organizing',
|
||||||
|
hosting: 'Hosting / MC',
|
||||||
|
permits: 'Permits & Council',
|
||||||
|
sponsorship: 'Sponsorship / Brand Deals',
|
||||||
|
promotion: 'Promotion & Marketing',
|
||||||
|
livestream: 'Livestream / Coverage',
|
||||||
|
sound: 'Sound & Music',
|
||||||
|
ramps: 'Ramps & Obstacles',
|
||||||
|
safety: 'Safety & First Aid',
|
||||||
|
vendors: 'Food & Vendors',
|
||||||
|
media: 'Photo / Video Coverage',
|
||||||
|
registration: 'Sign-ups & Brackets',
|
||||||
|
};
|
||||||
|
|
||||||
|
// How big the events they run are.
|
||||||
|
export const ORGANIZER_SCALE_LABELS: Record<string, string> = {
|
||||||
|
grassroots: 'Grassroots',
|
||||||
|
local: 'Local',
|
||||||
|
regional: 'Regional',
|
||||||
|
national: 'National',
|
||||||
|
international: 'International',
|
||||||
|
pro: 'Pro Circuit',
|
||||||
|
};
|
||||||
|
|
||||||
// ----- Crews (community identity, map-anchored) -----
|
// ----- Crews (community identity, map-anchored) -----
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3614,3 +3664,40 @@ export function parseSkateJudge(event: NostrEvent): SkateJudge | null {
|
||||||
eventsJudged: event.tags.find(([name]) => name === 'events_judged')?.[1],
|
eventsJudged: event.tags.find(([name]) => name === 'events_judged')?.[1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkateOrganizer {
|
||||||
|
id: string;
|
||||||
|
pubkey: string;
|
||||||
|
identifier: string;
|
||||||
|
featuredUntil?: number;
|
||||||
|
title: string; // organizer / crew name
|
||||||
|
bio: string;
|
||||||
|
geohash?: string;
|
||||||
|
location?: string;
|
||||||
|
image?: string;
|
||||||
|
skills: string[]; // event types + services + scale tags
|
||||||
|
experience?: string; // years organizing
|
||||||
|
rate?: string; // fee / budget for putting on an event
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
website?: string;
|
||||||
|
portfolio?: string; // past events gallery / results link
|
||||||
|
social?: string;
|
||||||
|
eventsHosted?: string; // freeform count / notable events run
|
||||||
|
noffer?: string;
|
||||||
|
entityType: EntityType;
|
||||||
|
members: string[];
|
||||||
|
crewRef?: string;
|
||||||
|
createdAt: number;
|
||||||
|
event: NostrEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSkateOrganizer(event: NostrEvent): SkateOrganizer | null {
|
||||||
|
if (event.kind !== SKATE_ORGANIZER_KIND) return null;
|
||||||
|
const base = parseCraftBase(event, 'skateorganizer');
|
||||||
|
if (!base) return null;
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
eventsHosted: event.tags.find(([name]) => name === 'events_hosted')?.[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
||||||
import { isAdminPubkey } from '@/lib/admins';
|
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 } 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 } from '@/lib/types';
|
||||||
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
||||||
import { OfficialBadge } from '@/components/OfficialBadge';
|
import { OfficialBadge } from '@/components/OfficialBadge';
|
||||||
import { ProBadge } from '@/components/ProBadge';
|
import { ProBadge } from '@/components/ProBadge';
|
||||||
|
|
@ -48,6 +48,7 @@ import {
|
||||||
Dumbbell,
|
Dumbbell,
|
||||||
Music,
|
Music,
|
||||||
Gavel,
|
Gavel,
|
||||||
|
Tent,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
|
@ -136,6 +137,7 @@ export default function CoachDetailPage() {
|
||||||
trainer: { text: 'text-emerald-400', bg: 'bg-emerald-600', border: 'border-emerald-500/30', hover: 'hover:bg-emerald-500/10 border-emerald-500' },
|
trainer: { text: 'text-emerald-400', bg: 'bg-emerald-600', border: 'border-emerald-500/30', hover: 'hover:bg-emerald-500/10 border-emerald-500' },
|
||||||
musician: { text: 'text-sky-400', bg: 'bg-sky-500', border: 'border-sky-500/30', hover: 'hover:bg-sky-500/10 border-sky-500' },
|
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' },
|
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' },
|
||||||
};
|
};
|
||||||
const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach;
|
const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach;
|
||||||
|
|
||||||
|
|
@ -154,6 +156,7 @@ export default function CoachDetailPage() {
|
||||||
const TYPE_ICONS: Record<string, typeof GraduationCap> = {
|
const TYPE_ICONS: Record<string, typeof GraduationCap> = {
|
||||||
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors,
|
||||||
artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel,
|
artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel,
|
||||||
|
organizer: Tent,
|
||||||
};
|
};
|
||||||
const TypeIcon = TYPE_ICONS[profile.profileType] ?? GraduationCap;
|
const TypeIcon = TYPE_ICONS[profile.profileType] ?? GraduationCap;
|
||||||
|
|
||||||
|
|
@ -162,6 +165,7 @@ export default function CoachDetailPage() {
|
||||||
coach: SKATE_COACH_KIND, filmer: SKATE_FILMER_KIND, editor: SKATE_EDITOR_KIND,
|
coach: SKATE_COACH_KIND, filmer: SKATE_FILMER_KIND, editor: SKATE_EDITOR_KIND,
|
||||||
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, musician: SKATE_MUSICIAN_KIND, judge: SKATE_JUDGE_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,
|
||||||
};
|
};
|
||||||
const profileKind = KINDS[profile.profileType] ?? SKATE_COACH_KIND;
|
const profileKind = KINDS[profile.profileType] ?? SKATE_COACH_KIND;
|
||||||
const isCollective = profile.entityType === 'collective' && profile.members.length > 0;
|
const isCollective = profile.entityType === 'collective' && profile.members.length > 0;
|
||||||
|
|
@ -179,6 +183,7 @@ export default function CoachDetailPage() {
|
||||||
trainer: { ...TRAINER_FOCUS_LABELS, ...TRAINER_INJURY_LABELS, ...TRAINER_CREDENTIAL_LABELS },
|
trainer: { ...TRAINER_FOCUS_LABELS, ...TRAINER_INJURY_LABELS, ...TRAINER_CREDENTIAL_LABELS },
|
||||||
musician: { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS },
|
musician: { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS },
|
||||||
judge: { ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS },
|
judge: { ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS },
|
||||||
|
organizer: { ...ORGANIZER_EVENT_LABELS, ...ORGANIZER_SERVICE_LABELS, ...ORGANIZER_SCALE_LABELS },
|
||||||
};
|
};
|
||||||
const DESCRIPTIONS_BY_TYPE: Record<string, Record<string, string>> = {
|
const DESCRIPTIONS_BY_TYPE: Record<string, Record<string, string>> = {
|
||||||
coach: COACH_SKILL_DESCRIPTIONS,
|
coach: COACH_SKILL_DESCRIPTIONS,
|
||||||
|
|
@ -192,6 +197,7 @@ export default function CoachDetailPage() {
|
||||||
trainer: TRAINER_FOCUS_DESCRIPTIONS,
|
trainer: TRAINER_FOCUS_DESCRIPTIONS,
|
||||||
musician: {},
|
musician: {},
|
||||||
judge: {},
|
judge: {},
|
||||||
|
organizer: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSkillLabel = (skill: string) => LABELS_BY_TYPE[profile.profileType]?.[skill] || skill;
|
const getSkillLabel = (skill: string) => LABELS_BY_TYPE[profile.profileType]?.[skill] || skill;
|
||||||
|
|
@ -200,6 +206,7 @@ export default function CoachDetailPage() {
|
||||||
const TYPE_NOUNS: Record<string, string> = {
|
const TYPE_NOUNS: Record<string, string> = {
|
||||||
coach: 'coach', filmer: 'filmer', editor: 'video editor', photographer: 'photographer', builder: 'builder',
|
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',
|
artist: 'artist', graffiti: 'graffiti artist', tattoo: 'tattoo artist', trainer: 'trainer', musician: 'musician', judge: 'event judge',
|
||||||
|
organizer: 'event organizer',
|
||||||
};
|
};
|
||||||
const typeNoun = TYPE_NOUNS[profile.profileType] ?? 'profile';
|
const typeNoun = TYPE_NOUNS[profile.profileType] ?? 'profile';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,9 @@ import {
|
||||||
JUDGE_FORMAT_LABELS,
|
JUDGE_FORMAT_LABELS,
|
||||||
JUDGE_SPECIALTY_LABELS,
|
JUDGE_SPECIALTY_LABELS,
|
||||||
JUDGE_LEVEL_LABELS,
|
JUDGE_LEVEL_LABELS,
|
||||||
|
ORGANIZER_EVENT_LABELS,
|
||||||
|
ORGANIZER_SERVICE_LABELS,
|
||||||
|
ORGANIZER_SCALE_LABELS,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import {
|
import {
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
|
|
@ -71,6 +74,7 @@ import {
|
||||||
Shield,
|
Shield,
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Tent,
|
||||||
} 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';
|
||||||
|
|
@ -141,6 +145,13 @@ const JUDGE_ALL_LABELS: Record<string, string> = {
|
||||||
...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS,
|
...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ORGANIZER_EVENT_ENTRIES = Object.entries(ORGANIZER_EVENT_LABELS);
|
||||||
|
const ORGANIZER_SERVICE_ENTRIES = Object.entries(ORGANIZER_SERVICE_LABELS);
|
||||||
|
const ORGANIZER_SCALE_ENTRIES = Object.entries(ORGANIZER_SCALE_LABELS);
|
||||||
|
const ORGANIZER_ALL_LABELS: Record<string, string> = {
|
||||||
|
...ORGANIZER_EVENT_LABELS, ...ORGANIZER_SERVICE_LABELS, ...ORGANIZER_SCALE_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,
|
||||||
|
|
@ -153,7 +164,7 @@ 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';
|
| 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging' | 'organizing';
|
||||||
|
|
||||||
/** 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']> = {
|
||||||
|
|
@ -168,6 +179,7 @@ const SERVICE_TO_PROFILE_TYPE: Record<ServiceType, LabProfile['profileType']> =
|
||||||
training: 'trainer',
|
training: 'trainer',
|
||||||
music: 'musician',
|
music: 'musician',
|
||||||
judging: 'judge',
|
judging: 'judge',
|
||||||
|
organizing: 'organizer',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Per-category accent colours ─────────────────────────────────────────────
|
// ─── Per-category accent colours ─────────────────────────────────────────────
|
||||||
|
|
@ -294,6 +306,17 @@ const ACCENT = {
|
||||||
stripe: 'bg-yellow-500',
|
stripe: 'bg-yellow-500',
|
||||||
badgeSkill: 'bg-yellow-900/40 border-yellow-700/50 text-yellow-300',
|
badgeSkill: 'bg-yellow-900/40 border-yellow-700/50 text-yellow-300',
|
||||||
},
|
},
|
||||||
|
organizing: {
|
||||||
|
active: 'bg-teal-600 hover:bg-teal-700 text-white border-teal-600',
|
||||||
|
chip: 'bg-teal-600 text-white border-teal-600',
|
||||||
|
chipHover: 'hover:border-teal-500/60 hover:text-teal-400',
|
||||||
|
border: 'border-teal-500/40',
|
||||||
|
glow: 'shadow-teal-500/20',
|
||||||
|
icon: 'text-teal-400',
|
||||||
|
badge: 'bg-teal-500/15 text-teal-400 border-teal-500/30',
|
||||||
|
stripe: 'bg-teal-600',
|
||||||
|
badgeSkill: 'bg-teal-900/40 border-teal-700/50 text-teal-300',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── FAB — offer services ─────────────────────────────────────────────────────
|
// ─── FAB — offer services ─────────────────────────────────────────────────────
|
||||||
|
|
@ -324,6 +347,7 @@ function OfferFAB() {
|
||||||
{ to: '/become/trainer', label: 'Offer Training / Physio', color: 'bg-emerald-600 hover:bg-emerald-700', icon: Dumbbell },
|
{ to: '/become/trainer', label: 'Offer Training / Physio', color: 'bg-emerald-600 hover:bg-emerald-700', icon: Dumbbell },
|
||||||
{ to: '/become/musician', label: 'Add Music Profile', color: 'bg-sky-500 hover:bg-sky-600', icon: Music },
|
{ 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/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: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard },
|
{ to: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -401,6 +425,7 @@ function getSkillLabelForType(skill: string, serviceType: ServiceType): string {
|
||||||
if (serviceType === 'training') return TRAINER_ALL_LABELS[skill] || skill;
|
if (serviceType === 'training') return TRAINER_ALL_LABELS[skill] || skill;
|
||||||
if (serviceType === 'music') return MUSICIAN_ALL_LABELS[skill] || skill;
|
if (serviceType === 'music') return MUSICIAN_ALL_LABELS[skill] || skill;
|
||||||
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;
|
||||||
return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill;
|
return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,6 +459,7 @@ function LabToolbar({
|
||||||
{ key: 'training', label: 'Trainers', icon: Dumbbell },
|
{ key: 'training', label: 'Trainers', icon: Dumbbell },
|
||||||
{ key: 'music', label: 'Music', icon: Music },
|
{ key: 'music', label: 'Music', icon: Music },
|
||||||
{ key: 'judging', label: 'Judges', icon: Gavel },
|
{ key: 'judging', label: 'Judges', icon: Gavel },
|
||||||
|
{ key: 'organizing', label: 'Organizers', icon: Tent },
|
||||||
];
|
];
|
||||||
|
|
||||||
const accent = ACCENT[serviceType];
|
const accent = ACCENT[serviceType];
|
||||||
|
|
@ -689,6 +715,12 @@ function FilterDrawer({
|
||||||
{ title: 'Specialty', entries: JUDGE_SPECIALTY_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
|
{ title: 'Specialty', entries: JUDGE_SPECIALTY_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
|
||||||
{ title: 'Level / Circuit', entries: JUDGE_LEVEL_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
|
{ title: 'Level / Circuit', entries: JUDGE_LEVEL_ENTRIES, activeColor: 'bg-yellow-400 text-black border-yellow-400' },
|
||||||
]
|
]
|
||||||
|
: serviceType === 'organizing'
|
||||||
|
? [
|
||||||
|
{ title: 'Event Types', entries: ORGANIZER_EVENT_ENTRIES, activeColor: accent.chip },
|
||||||
|
{ 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' },
|
||||||
|
]
|
||||||
: [
|
: [
|
||||||
{ title: 'Edit Type', entries: EDITOR_SERVICE_ENTRIES, activeColor: accent.chip },
|
{ 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' },
|
{ title: 'Software & Tools', entries: EDITOR_SKILL_ENTRIES, activeColor: 'bg-purple-400 text-white border-purple-400' },
|
||||||
|
|
@ -864,6 +896,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
const PROFILE_TO_SERVICE: Record<LabProfile['profileType'], ServiceType> = {
|
const PROFILE_TO_SERVICE: Record<LabProfile['profileType'], ServiceType> = {
|
||||||
coach: 'coaching', filmer: 'filming', editor: 'editing', photographer: 'photography', builder: 'building',
|
coach: 'coaching', filmer: 'filming', editor: 'editing', photographer: 'photography', builder: 'building',
|
||||||
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',
|
||||||
};
|
};
|
||||||
const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType];
|
const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType];
|
||||||
const accent = ACCENT[serviceType];
|
const accent = ACCENT[serviceType];
|
||||||
|
|
@ -871,6 +904,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
const TYPE_ICONS: Record<ServiceType, React.ComponentType<{ className?: string }>> = {
|
const TYPE_ICONS: Record<ServiceType, React.ComponentType<{ className?: string }>> = {
|
||||||
coaching: GraduationCap, filming: Video, editing: Scissors, photography: Camera, building: HardHat,
|
coaching: GraduationCap, filming: Video, editing: Scissors, photography: Camera, building: HardHat,
|
||||||
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,
|
||||||
};
|
};
|
||||||
const TypeIcon = TYPE_ICONS[serviceType];
|
const TypeIcon = TYPE_ICONS[serviceType];
|
||||||
|
|
||||||
|
|
@ -1091,6 +1125,14 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
||||||
to: '/become/judge',
|
to: '/become/judge',
|
||||||
color: 'bg-yellow-500 hover:bg-yellow-600 text-black',
|
color: 'bg-yellow-500 hover:bg-yellow-600 text-black',
|
||||||
},
|
},
|
||||||
|
organizing: {
|
||||||
|
icon: Tent,
|
||||||
|
title: 'No organizers yet',
|
||||||
|
body: 'Put on jams, comps, meetups or demos? List yourself so skaters can find and book you.',
|
||||||
|
cta: 'Organize Events',
|
||||||
|
to: '/become/organizer',
|
||||||
|
color: 'bg-teal-600 hover:bg-teal-700',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const c = configs[serviceType];
|
const c = configs[serviceType];
|
||||||
|
|
@ -1141,7 +1183,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'];
|
const valid: ServiceType[] = ['coaching', 'filming', 'editing', 'photography', 'building', 'art', 'graffiti', 'tattoo', 'training', 'music', 'judging', 'organizing'];
|
||||||
return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching';
|
return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -1190,7 +1232,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,
|
art: 0, graffiti: 0, tattoo: 0, training: 0, music: 0, judging: 0, organizing: 0,
|
||||||
};
|
};
|
||||||
if (!profiles) return empty;
|
if (!profiles) return empty;
|
||||||
const byType = (t: ServiceType) =>
|
const byType = (t: ServiceType) =>
|
||||||
|
|
@ -1207,6 +1249,7 @@ export default function LabPage() {
|
||||||
training: byType('training'),
|
training: byType('training'),
|
||||||
music: byType('music'),
|
music: byType('music'),
|
||||||
judging: byType('judging'),
|
judging: byType('judging'),
|
||||||
|
organizing: byType('organizing'),
|
||||||
};
|
};
|
||||||
}, [profiles]);
|
}, [profiles]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue