diff --git a/NIP.md b/NIP.md index 18e1ddb..e1cca74 100644 --- a/NIP.md +++ b/NIP.md @@ -1422,6 +1422,7 @@ They share a common base schema and are distinguished by kind and a self-tag. | 35009 | Tattoo artist | `skatetattoo` | | 35010 | Trainer / Physiotherapist | `skatetrainer` | | 35011 | Musician / Band | `skatemusician` | +| 35012 | Event Judge | `skatejudge` | > Kind `35006` is **not** a Lab provider — it is the Skate Crew (see below). @@ -1822,12 +1823,35 @@ A band can be a `collective` with member `p` tags (bilateral NIP-32 attestation, same as other collectives), and may link to its skate crew via an `a` tag (`35006:pubkey:d`). +### Event Judge (kind 35012) + +Officials who judge skate events — from flat-ground Game of S.K.A.T.E. and best-trick +jams to full contests, am-series qualifiers, and touring stops (e.g. a World +Destruction Tour). A judge lists the formats they run, their judging specialty, +and the level/circuit they have worked so organizers can book credible, fair +officiating. Uses the common base schema. + +Categories encoded as `t` tags: + +- **Event formats**: `game_of_skate`, `best_trick`, `jam`, `contest`, `street_league`, `bowl`, `mini_ramp`, `park`, `street`, `downhill`, `freestyle`, `world_tour`, `am_series`, `local_jam`, `online` +- **Specialty**: `head_judge`, `scoring`, `mc`, `format_design`, `rules`, `brackets`, `livestream`, `am_scout` +- **Level / circuit**: `local`, `regional`, `national`, `international`, `pro` + +Judge-specific optional tags: + +| Tag | Description | +|-----|-------------| +| `events_judged` | Freeform note on notable events / comps / tours officiated (≤ base limits). | + +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`). + ### Querying Lab Profiles ```json { - "kinds": [35001, 35002, 35003, 35004, 35005, 35007, 35008, 35009, 35010, 35011], - "#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer", "skatebuilder", "skateartist", "skategraffiti", "skatetattoo", "skatetrainer", "skatemusician"], + "kinds": [35001, 35002, 35003, 35004, 35005, 35007, 35008, 35009, 35010, 35011, 35012], + "#t": ["skatecoach", "skatefilmer", "skateeditor", "skatephotographer", "skatebuilder", "skateartist", "skategraffiti", "skatetattoo", "skatetrainer", "skatemusician", "skatejudge"], "limit": 100 } ``` diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 48045a3..095634d 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -36,6 +36,7 @@ const BecomeGraffitiPage = lazy(() => import("./pages/BecomeGraffitiPage")); const BecomeTattooPage = lazy(() => import("./pages/BecomeTattooPage")); const BecomeTrainerPage = lazy(() => import("./pages/BecomeTrainerPage")); const BecomeMusicianPage = lazy(() => import("./pages/BecomeMusicianPage")); +const BecomeJudgePage = lazy(() => import("./pages/BecomeJudgePage")); const CrewsPage = lazy(() => import("./pages/CrewsPage")); const CreateCrewPage = lazy(() => import("./pages/CreateCrewPage")); const EditCrewPage = lazy(() => import("./pages/EditCrewPage")); @@ -101,6 +102,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/hooks/useLabProfiles.ts b/src/hooks/useLabProfiles.ts index 7596aa7..2470d95 100644 --- a/src/hooks/useLabProfiles.ts +++ b/src/hooks/useLabProfiles.ts @@ -6,7 +6,7 @@ import { parseSkateArtist, parseSkateGraffiti, parseSkateTattoo, parseSkateTrainer, parseSkateMusician, parseSkateJudge, isFeaturedActive, type SkateCoach, type SkateFilmer, type SkateEditor, type SkatePhotographer, type SkateBuilder, - type SkateArtist, type SkateGraffiti, type SkateTattoo, type SkateTrainer, type SkateMusician, + type SkateArtist, type SkateGraffiti, type SkateTattoo, type SkateTrainer, type SkateMusician, type SkateJudge, } from '@/lib/types'; import type { NostrEvent } from '@nostrify/nostrify'; import { useSkateRelays } from '@/hooks/useSkateRelays'; @@ -18,7 +18,7 @@ export type LabProfileType = export type LabProfile = ( | SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder - | SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician + | SkateArtist | SkateGraffiti | SkateTattoo | SkateTrainer | SkateMusician | SkateJudge ) & { profileType: LabProfileType; }; @@ -26,7 +26,6 @@ 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, - 'judge' ]; const LAB_SELF_TAGS = [ @@ -77,6 +76,10 @@ export function parseLabProfile(event: NostrEvent): LabProfile | null { const p = parseSkateMusician(event); return p ? { ...p, profileType: 'musician' } : null; } + case SKATE_JUDGE_KIND: { + const p = parseSkateJudge(event); + return p ? { ...p, profileType: 'judge' } : null; + } default: return null; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 7877ac2..15ac5c3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1214,6 +1214,49 @@ export const MUSICIAN_ACT_LABELS: Record = { collective: 'Collective', }; +// ----- Judges (skate event officials) ----- + +// Event formats a judge is experienced with. Covers everything from a casket +// of flat-ground S.K.A.T. to the biggest touring contest circuits. +export const JUDGE_FORMAT_LABELS: Record = { + game_of_skate: 'Game of S.K.A.T.E.', + best_trick: 'Best Trick', + jam: 'Jam Format', + contest: 'Contest / Comp', + street_league: 'Street League Style', + bowl: 'Bowl / Vert', + mini_ramp: 'Mini Ramp', + park: 'Park', + street: 'Street', + downhill: 'Downhill / Longboard', + freestyle: 'Freestyle', + world_tour: 'World Destruction Tour', + am_series: 'Am Series / Qualifiers', + local_jam: 'Local Jam / Grassroots', + online: 'Online / IG Contest', +}; + +// What the judge specializes in evaluating. +export const JUDGE_SPECIALTY_LABELS: Record = { + head_judge: 'Head Judge', + scoring: 'Scoring / Tabulation', + mc: 'MC / Announcer', + format_design: 'Format Design', + rules: 'Rules / Officiating', + brackets: 'Brackets / Heats', + livestream: 'Livestream Judging', + am_scout: 'Am Scouting', +}; + +// Level / circuit the judge has worked. +export const JUDGE_LEVEL_LABELS: Record = { + local: 'Local', + regional: 'Regional', + national: 'National', + international: 'International', + pro: 'Pro Circuit', +}; + // ----- Crews (community identity, map-anchored) ----- /** @@ -2804,25 +2847,35 @@ export interface SkateJudge { id: string; pubkey: string; identifier: string; - expertise: string; - experience: string; - events_judged: string; + featuredUntil?: number; + title: string; // judge name / handle + bio: string; + geohash?: string; + location?: string; + image?: string; + skills: string[]; // format + specialty + level tags + experience?: string; // years judging + rate?: string; // booking rate for events + phone?: string; + email?: string; + website?: string; + portfolio?: string; // results archive / profile link + social?: string; + eventsJudged?: string; // freeform count / notable events judged + noffer?: string; entityType: EntityType; + members: string[]; + crewRef?: string; createdAt: number; event: NostrEvent; } export function parseSkateJudge(event: NostrEvent): SkateJudge | null { if (event.kind !== SKATE_JUDGE_KIND) return null; - - const judge = { - id: event.id, - pubkey: event.pubkey, - identifier: event.tags.find(([name]) => name === 'd')?.[1] || '', - expertise: event.tags.find(([name]) => name === 'expertise')?.[1] || '', - experience: event.tags.find(([name]) => name === 'experience')?.[1] || '', - events_judged: event.tags.find(([name]) => name === 'events_judged')?.[1] || '0', + const base = parseCraftBase(event, 'skatejudge'); + if (!base) return null; + return { + ...base, + eventsJudged: event.tags.find(([name]) => name === 'events_judged')?.[1], }; - - return judge; } diff --git a/src/pages/BecomeJudgePage.tsx b/src/pages/BecomeJudgePage.tsx new file mode 100644 index 0000000..801e7f4 --- /dev/null +++ b/src/pages/BecomeJudgePage.tsx @@ -0,0 +1,50 @@ +import { useSeoMeta } from '@unhead/react'; +import { Gavel } from 'lucide-react'; +import { CraftProfileForm } from '@/components/CraftProfileForm'; +import { + SKATE_JUDGE_KIND, + JUDGE_FORMAT_LABELS, + JUDGE_SPECIALTY_LABELS, + JUDGE_LEVEL_LABELS, +} from '@/lib/types'; + +export default function BecomeJudgePage() { + useSeoMeta({ + title: 'Judge Skate Events - SkateSpots', + description: + 'List yourself as a skate event judge — Game of S.K.A.T.E., best trick jams, contests, am series and world tour stops. Get booked to run fair, credible events.', + }); + + return ( + + ); +} diff --git a/src/pages/CoachDetailPage.tsx b/src/pages/CoachDetailPage.tsx index 595f61b..0e66795 100644 --- a/src/pages/CoachDetailPage.tsx +++ b/src/pages/CoachDetailPage.tsx @@ -20,7 +20,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { isAdminPubkey } from '@/lib/admins'; import { genUserName } from '@/lib/genUserName'; -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, 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 } 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 } from '@/lib/types'; import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl'; import { OfficialBadge } from '@/components/OfficialBadge'; import { ProBadge } from '@/components/ProBadge'; @@ -46,6 +46,7 @@ import { Brush, Dumbbell, Music, + Gavel, } from 'lucide-react'; import { nip19 } from 'nostr-tools'; @@ -132,12 +133,13 @@ export default function CoachDetailPage() { tattoo: { text: 'text-rose-400', bg: 'bg-rose-600', border: 'border-rose-500/30', hover: 'hover:bg-rose-500/10 border-rose-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' }, + judge: { text: 'text-yellow-400', bg: 'bg-yellow-500', border: 'border-yellow-500/30', hover: 'hover:bg-yellow-500/10 border-yellow-500' }, }; const accent = ACCENTS[profile.profileType] ?? ACCENTS.coach; const TYPE_ICONS: Record = { coach: GraduationCap, filmer: Video, photographer: Camera, builder: HardHat, editor: Scissors, - artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, + artist: Palette, graffiti: SprayCan, tattoo: Brush, trainer: Dumbbell, musician: Music, judge: Gavel, }; const TypeIcon = TYPE_ICONS[profile.profileType] ?? GraduationCap; @@ -145,7 +147,7 @@ export default function CoachDetailPage() { const KINDS: Record = { coach: SKATE_COACH_KIND, filmer: SKATE_FILMER_KIND, editor: SKATE_EDITOR_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, + graffiti: SKATE_GRAFFITI_KIND, tattoo: SKATE_TATTOO_KIND, trainer: SKATE_TRAINER_KIND, musician: SKATE_MUSICIAN_KIND, judge: SKATE_JUDGE_KIND, }; const profileKind = KINDS[profile.profileType] ?? SKATE_COACH_KIND; const isCollective = profile.entityType === 'collective' && profile.members.length > 0; @@ -162,6 +164,7 @@ export default function CoachDetailPage() { tattoo: { ...TATTOO_STYLE_LABELS, ...TATTOO_METHOD_LABELS }, trainer: { ...TRAINER_FOCUS_LABELS, ...TRAINER_INJURY_LABELS, ...TRAINER_CREDENTIAL_LABELS }, musician: { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS }, + judge: { ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS }, }; const DESCRIPTIONS_BY_TYPE: Record> = { coach: COACH_SKILL_DESCRIPTIONS, @@ -174,6 +177,7 @@ export default function CoachDetailPage() { tattoo: TATTOO_STYLE_DESCRIPTIONS, trainer: TRAINER_FOCUS_DESCRIPTIONS, musician: {}, + judge: {}, }; const getSkillLabel = (skill: string) => LABELS_BY_TYPE[profile.profileType]?.[skill] || skill; @@ -181,14 +185,14 @@ export default function CoachDetailPage() { const TYPE_NOUNS: Record = { coach: 'coach', filmer: 'filmer', editor: 'video editor', photographer: 'photographer', builder: 'builder', - artist: 'artist', graffiti: 'graffiti artist', tattoo: 'tattoo artist', trainer: 'trainer', musician: 'musician', + artist: 'artist', graffiti: 'graffiti artist', tattoo: 'tattoo artist', trainer: 'trainer', musician: 'musician', judge: 'event judge', }; const typeNoun = TYPE_NOUNS[profile.profileType] ?? 'profile'; const SKILLS_TITLES: Record = { coach: 'Skills', photographer: 'Services & Gear', builder: 'What We Build', artist: 'What They Make', graffiti: 'Styles & Work', tattoo: 'Styles', - trainer: 'Focus & Specialties', musician: 'Genres & Instruments', + trainer: 'Focus & Specialties', musician: 'Genres & Instruments', judge: 'Formats & Specialties', }; const skillsTitle = SKILLS_TITLES[profile.profileType] ?? 'Services & Skills'; diff --git a/src/pages/CommunityStatsPage.tsx b/src/pages/CommunityStatsPage.tsx index 689da38..1cce167 100644 --- a/src/pages/CommunityStatsPage.tsx +++ b/src/pages/CommunityStatsPage.tsx @@ -280,7 +280,7 @@ export default function CommunityStatsPage() { - + diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx index c4535e1..0e23be5 100644 --- a/src/pages/LabPage.tsx +++ b/src/pages/LabPage.tsx @@ -40,6 +40,9 @@ import { TRAINER_CREDENTIAL_LABELS, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS, + JUDGE_FORMAT_LABELS, + JUDGE_SPECIALTY_LABELS, + JUDGE_LEVEL_LABELS, } from '@/lib/types'; import { GraduationCap, @@ -56,6 +59,7 @@ import { Brush, Dumbbell, Music, + Gavel, CreditCard, X, ChevronRight, @@ -126,6 +130,13 @@ const MUSICIAN_ALL_LABELS: Record = { ...MUSICIAN_GENRE_LABELS, ...MUSICIAN_INSTRUMENT_LABELS, }; +const JUDGE_FORMAT_ENTRIES = Object.entries(JUDGE_FORMAT_LABELS); +const JUDGE_SPECIALTY_ENTRIES = Object.entries(JUDGE_SPECIALTY_LABELS); +const JUDGE_LEVEL_ENTRIES = Object.entries(JUDGE_LEVEL_LABELS); +const JUDGE_ALL_LABELS: Record = { + ...JUDGE_FORMAT_LABELS, ...JUDGE_SPECIALTY_LABELS, ...JUDGE_LEVEL_LABELS, +}; + // All photographer label maps merged — for resolving a chip label regardless of category const PHOTO_ALL_LABELS: Record = { ...PHOTO_SERVICE_LABELS, @@ -138,7 +149,7 @@ const PHOTO_ALL_LABELS: Record = { type ServiceType = | 'coaching' | 'filming' | 'editing' | 'photography' | 'building' - | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music'; + | 'art' | 'graffiti' | 'tattoo' | 'training' | 'music' | 'judging'; /** Map a ServiceType to the LabProfile.profileType it filters. */ const SERVICE_TO_PROFILE_TYPE: Record = { @@ -152,6 +163,7 @@ const SERVICE_TO_PROFILE_TYPE: Record = tattoo: 'tattoo', training: 'trainer', music: 'musician', + judging: 'judge', }; // ─── Per-category accent colours ───────────────────────────────────────────── @@ -267,6 +279,17 @@ const ACCENT = { stripe: 'bg-sky-500', badgeSkill: 'bg-sky-900/40 border-sky-700/50 text-sky-300', }, + judging: { + active: 'bg-yellow-500 hover:bg-yellow-600 text-black border-yellow-500', + chip: 'bg-yellow-500 text-black border-yellow-500', + chipHover: 'hover:border-yellow-500/60 hover:text-yellow-400', + border: 'border-yellow-500/40', + glow: 'shadow-yellow-500/20', + icon: 'text-yellow-400', + badge: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30', + stripe: 'bg-yellow-500', + badgeSkill: 'bg-yellow-900/40 border-yellow-700/50 text-yellow-300', + }, }; // ─── FAB — offer services ───────────────────────────────────────────────────── @@ -296,6 +319,7 @@ function OfferFAB() { { to: '/become-tattoo', label: 'Offer Tattoo', color: 'bg-rose-600 hover:bg-rose-700', icon: Brush }, { 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-judge', label: 'Judge Events', color: 'bg-yellow-500 hover:bg-yellow-600 text-black', icon: Gavel }, { to: '/card', label: 'Get Rider Card', color: 'bg-[#ccff00]/20 hover:bg-[#ccff00]/30 border border-[#ccff00]/50 text-[#ccff00]', icon: CreditCard }, ]; @@ -373,6 +397,7 @@ function getSkillLabelForType(skill: string, serviceType: ServiceType): string { if (serviceType === 'tattoo') return TATTOO_ALL_LABELS[skill] || skill; if (serviceType === 'training') return TRAINER_ALL_LABELS[skill] || skill; if (serviceType === 'music') return MUSICIAN_ALL_LABELS[skill] || skill; + if (serviceType === 'judging') return JUDGE_ALL_LABELS[skill] || skill; return EDITOR_SERVICE_LABELS[skill] || EDITOR_SKILL_LABELS[skill] || skill; } @@ -405,6 +430,7 @@ function LabToolbar({ { key: 'tattoo', label: 'Tattoo', icon: Brush }, { key: 'training', label: 'Trainers', icon: Dumbbell }, { key: 'music', label: 'Music', icon: Music }, + { key: 'judging', label: 'Judges', icon: Gavel }, ]; const accent = ACCENT[serviceType]; @@ -655,6 +681,12 @@ function FilterDrawer({ { title: 'Genre', entries: MUSICIAN_GENRE_ENTRIES, activeColor: accent.chip }, { title: 'Instruments & Role', entries: MUSICIAN_INSTRUMENT_ENTRIES, activeColor: 'bg-sky-400 text-white border-sky-400' }, ] + : serviceType === 'judging' + ? [ + { title: 'Event Formats', entries: JUDGE_FORMAT_ENTRIES, activeColor: accent.chip }, + { 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: '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' }, @@ -829,14 +861,14 @@ function ProfileCard({ profile }: { profile: LabProfile }) { const PROFILE_TO_SERVICE: Record = { coach: 'coaching', filmer: 'filming', editor: 'editing', photographer: 'photography', builder: 'building', - artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music', + artist: 'art', graffiti: 'graffiti', tattoo: 'tattoo', trainer: 'training', musician: 'music', judge: 'judging', }; const serviceType: ServiceType = PROFILE_TO_SERVICE[profile.profileType]; const accent = ACCENT[serviceType]; const TYPE_ICONS: Record> = { coaching: GraduationCap, filming: Video, editing: Scissors, photography: Camera, building: HardHat, - art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music, + art: Palette, graffiti: SprayCan, tattoo: Brush, training: Dumbbell, music: Music, judging: Gavel, }; const TypeIcon = TYPE_ICONS[serviceType]; @@ -1049,6 +1081,14 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo to: '/become-musician', color: 'bg-sky-500 hover:bg-sky-600', }, + judging: { + icon: Gavel, + title: 'No judges yet', + body: 'Run a fair contest? List yourself to judge Game of S.K.A.T.E., best trick jams, contests and touring stops.', + cta: 'Judge Events', + to: '/become-judge', + color: 'bg-yellow-500 hover:bg-yellow-600 text-black', + }, }; const c = configs[serviceType]; @@ -1098,7 +1138,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']; + const valid: ServiceType[] = ['coaching', 'filming', 'editing', 'photography', 'building', 'art', 'graffiti', 'tattoo', 'training', 'music', 'judging']; return c && (valid as string[]).includes(c) ? (c as ServiceType) : 'coaching'; })(); @@ -1145,7 +1185,11 @@ export default function LabPage() { // Counts per tab (unfiltered by skills/search/location, just by type) const counts = useMemo>(() => { - if (!profiles) return { coaching: 0, filming: 0, editing: 0, photography: 0, building: 0 }; + const empty: Record = { + coaching: 0, filming: 0, editing: 0, photography: 0, building: 0, + art: 0, graffiti: 0, tattoo: 0, training: 0, music: 0, judging: 0, + }; + if (!profiles) return empty; const byType = (t: ServiceType) => profiles.filter((p) => p.profileType === SERVICE_TO_PROFILE_TYPE[t]).length; return { @@ -1159,6 +1203,7 @@ export default function LabPage() { tattoo: byType('tattoo'), training: byType('training'), music: byType('music'), + judging: byType('judging'), }; }, [profiles]);