refactor: consolidate become-* routes + migrate empty states to shared component

Empty states (P4 follow-up):
- CrewsPage and EventsPage now render the shared <EmptyState> instead of
  their own local copies, keeping spacing/actions/footer consistent.

Become-role routes (P1):
- Add src/lib/becomeRoles.tsx: single registry mapping each role slug to
  either a CraftProfileForm config (artist/graffiti/musician/judge/tattoo/
  trainer) or a lazy bespoke page (coach/filmer/editor/photographer/
  builder/skater). The 6 craft roles were already thin config wrappers, so
  their configs move into the registry and the wrapper files are deleted.
- Replace 12 hardcoded /become-* routes + 12 imports with one
  /become/:role route (BecomeRolePage) that renders from the registry.
- Legacy /become-<slug> URLs now redirect to /become/<slug> so shared/
  bookmarked links keep working.
- Update all internal links (LabPage, Index, SkatersPage, SpotDetailPage,
  NIP19Page, SkaterDirectoryCard) to the canonical /become/<slug> paths.

Note: the 5 large bespoke forms were intentionally NOT merged into one
config — they carry distinct machinery (SpotPicker, wallet/Pro, geohashing)
and forcing them together would increase complexity, not reduce it.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-24 18:44:53 +07:00
parent e7e492c568
commit 16e943d4ef
17 changed files with 385 additions and 373 deletions

View file

@ -1,6 +1,7 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { HashRouter, Route, Routes } from "react-router-dom"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import { ScrollToTop } from "./components/ScrollToTop"; import { ScrollToTop } from "./components/ScrollToTop";
import { BECOME_ROLES } from "./lib/becomeRoles";
import { DeepLinkHandler } from "./components/DeepLinkHandler"; import { DeepLinkHandler } from "./components/DeepLinkHandler";
import { RouteErrorBoundary } from "./components/RouteErrorBoundary"; import { RouteErrorBoundary } from "./components/RouteErrorBoundary";
@ -28,18 +29,9 @@ const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage"));
const EventDetailPage = lazy(() => import("./pages/EventDetailPage")); const EventDetailPage = lazy(() => import("./pages/EventDetailPage"));
const OnboardingPage = lazy(() => import("./pages/OnboardingPage")); const OnboardingPage = lazy(() => import("./pages/OnboardingPage"));
const PyramidPage = lazy(() => import("./pages/PyramidPage")); const PyramidPage = lazy(() => import("./pages/PyramidPage"));
const BecomeCoachPage = lazy(() => import("./pages/BecomeCoachPage")); // All "become a <role>" flows now share a single param route backed by a role
const BecomeFilmerPage = lazy(() => import("./pages/BecomeFilmerPage")); // registry (see src/lib/becomeRoles.tsx) instead of 12 separate route lines.
const BecomeEditorPage = lazy(() => import("./pages/BecomeEditorPage")); const BecomeRolePage = lazy(() => import("./pages/BecomeRolePage"));
const BecomePhotographerPage = lazy(() => import("./pages/BecomePhotographerPage"));
const BecomeBuilderPage = lazy(() => import("./pages/BecomeBuilderPage"));
const BecomeArtistPage = lazy(() => import("./pages/BecomeArtistPage"));
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 BecomeSkaterPage = lazy(() => import("./pages/BecomeSkaterPage"));
const SkatersPage = lazy(() => import("./pages/SkatersPage")); const SkatersPage = lazy(() => import("./pages/SkatersPage"));
const CrewsPage = lazy(() => import("./pages/CrewsPage")); const CrewsPage = lazy(() => import("./pages/CrewsPage"));
const CreateCrewPage = lazy(() => import("./pages/CreateCrewPage")); const CreateCrewPage = lazy(() => import("./pages/CreateCrewPage"));
@ -79,6 +71,10 @@ function PageLoader() {
); );
} }
// Legacy `/become-<slug>` URLs are linked from many pages and may be
// bookmarked/shared, so each redirects to the unified `/become/<slug>` route.
const BECOME_SLUGS = Object.keys(BECOME_ROLES);
export function AppRouter() { export function AppRouter() {
return ( return (
<HashRouter> <HashRouter>
@ -100,19 +96,16 @@ export function AppRouter() {
<Route path="/lab" element={<LabPage />} /> <Route path="/lab" element={<LabPage />} />
{/* Legacy route redirect */} {/* Legacy route redirect */}
<Route path="/coaches" element={<LabPage />} /> <Route path="/coaches" element={<LabPage />} />
<Route path="/become-coach" element={<BecomeCoachPage />} /> {/* Unified role route + legacy /become-<slug> redirects */}
<Route path="/become-filmer" element={<BecomeFilmerPage />} /> <Route path="/become/:role" element={<BecomeRolePage />} />
<Route path="/become-editor" element={<BecomeEditorPage />} /> {BECOME_SLUGS.map((slug) => (
<Route path="/become-photographer" element={<BecomePhotographerPage />} /> <Route
<Route path="/become-builder" element={<BecomeBuilderPage />} /> key={slug}
<Route path="/become-artist" element={<BecomeArtistPage />} /> path={`/become-${slug}`}
<Route path="/become-graffiti" element={<BecomeGraffitiPage />} /> element={<Navigate to={`/become/${slug}`} replace />}
<Route path="/become-tattoo" element={<BecomeTattooPage />} /> />
<Route path="/become-trainer" element={<BecomeTrainerPage />} /> ))}
<Route path="/become-musician" element={<BecomeMusicianPage />} />
<Route path="/become-judge" element={<BecomeJudgePage />} />
<Route path="/skaters" element={<SkatersPage />} /> <Route path="/skaters" element={<SkatersPage />} />
<Route path="/become-skater" element={<BecomeSkaterPage />} />
<Route path="/crews" element={<CrewsPage />} /> <Route path="/crews" element={<CrewsPage />} />
<Route path="/create-crew" element={<CreateCrewPage />} /> <Route path="/create-crew" element={<CreateCrewPage />} />
<Route path="/edit-crew/:pubkey/:identifier" element={<EditCrewPage />} /> <Route path="/edit-crew/:pubkey/:identifier" element={<EditCrewPage />} />

View file

@ -20,7 +20,7 @@ export function SkaterDirectoryCard() {
if (!skater) { if (!skater) {
return ( return (
<Link <Link
to="/become-skater" to="/become/skater"
className="group flex items-center gap-3 p-4 rounded-2xl border-2 border-dashed border-[#ff0040]/30 bg-[#ff0040]/5 hover:border-[#ff0040]/60 hover:bg-[#ff0040]/10 transition-all" className="group flex items-center gap-3 p-4 rounded-2xl border-2 border-dashed border-[#ff0040]/30 bg-[#ff0040]/5 hover:border-[#ff0040]/60 hover:bg-[#ff0040]/10 transition-all"
> >
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[#ff0040]/15 flex items-center justify-center group-hover:bg-[#ff0040]/25 transition-colors"> <div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[#ff0040]/15 flex items-center justify-center group-hover:bg-[#ff0040]/25 transition-colors">
@ -42,7 +42,7 @@ export function SkaterDirectoryCard() {
return ( return (
<Link <Link
to="/become-skater" to="/become/skater"
className="group flex items-center gap-3 p-4 rounded-2xl border border-[#ff0040]/30 bg-gradient-to-r from-[#ff0040]/10 via-transparent to-transparent hover:border-[#ff0040]/50 transition-all" className="group flex items-center gap-3 p-4 rounded-2xl border border-[#ff0040]/30 bg-gradient-to-r from-[#ff0040]/10 via-transparent to-transparent hover:border-[#ff0040]/50 transition-all"
> >
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[#ff0040]/15 flex items-center justify-center"> <div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[#ff0040]/15 flex items-center justify-center">

230
src/lib/becomeRoles.tsx Normal file
View file

@ -0,0 +1,230 @@
import { lazy, type ComponentType, type LazyExoticComponent } from 'react';
import {
Palette, SprayCan, Music, Gavel, Brush, Dumbbell,
} from 'lucide-react';
import type { CraftFormConfig } from '@/components/CraftProfileForm';
import {
SKATE_ARTIST_KIND, ARTIST_SERVICE_LABELS, ARTIST_MEDIUM_LABELS, ARTIST_TOOL_LABELS,
SKATE_GRAFFITI_KIND, GRAFFITI_STYLE_LABELS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS,
SKATE_MUSICIAN_KIND, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS,
SKATE_JUDGE_KIND, JUDGE_FORMAT_LABELS, JUDGE_SPECIALTY_LABELS, JUDGE_LEVEL_LABELS,
SKATE_TATTOO_KIND, TATTOO_STYLE_LABELS, TATTOO_METHOD_LABELS,
SKATE_TRAINER_KIND, TRAINER_FOCUS_LABELS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS,
} from '@/lib/types';
/**
* Single source of truth for every "become a …" role.
*
* There are two flavours:
* 1. `craft` roles — thin config over the shared <CraftProfileForm>. All the
* per-role difference lives here in data, not in 7 near-identical files.
* 2. `page` roles — genuinely bespoke pages (Coach, Filmer, Editor,
* Photographer, Skater, Builder) that carry extra machinery
* (SpotPicker, wallet/Pro logic, geohashing, etc.). Forcing these into a
* config would create a monster; instead they stay as their own lazy
* components and are simply *routed* through the same /become/:role slug.
*
* The router uses this map so the whole feature is one param route + redirects
* from the legacy /become-<slug> URLs, instead of 12 hardcoded route lines.
*/
interface CraftRole {
kind: 'craft';
seoTitle: string;
seoDescription: string;
config: CraftFormConfig;
}
interface PageRole {
kind: 'page';
/** Lazily-loaded bespoke page component (default export). */
component: LazyExoticComponent<ComponentType>;
}
export type BecomeRole = CraftRole | PageRole;
export const BECOME_ROLES: Record<string, BecomeRole> = {
// ── Bespoke pages (unchanged — just registered here for routing) ──
coach: { kind: 'page', component: lazy(() => import('@/pages/BecomeCoachPage')) },
filmer: { kind: 'page', component: lazy(() => import('@/pages/BecomeFilmerPage')) },
editor: { kind: 'page', component: lazy(() => import('@/pages/BecomeEditorPage')) },
photographer: { kind: 'page', component: lazy(() => import('@/pages/BecomePhotographerPage')) },
builder: { kind: 'page', component: lazy(() => import('@/pages/BecomeBuilderPage')) },
skater: { kind: 'page', component: lazy(() => import('@/pages/BecomeSkaterPage')) },
// ── Craft roles (data-driven over CraftProfileForm) ──
artist: {
kind: 'craft',
seoTitle: 'Become a Designer / Artist - SkateSpots',
seoDescription: 'List your skate design & art — board graphics, logos, posters, merch and more.',
config: {
kind: SKATE_ARTIST_KIND,
selfTag: 'skateartist',
identifier: 'artist-profile',
heading: 'Designer / Artist',
tagline: 'Board graphics, logos, posters, merch — put your art on the scene',
noun: 'artist',
accentText: 'text-fuchsia-400',
accentBg: 'bg-fuchsia-600',
accentChip: 'bg-fuchsia-600 text-white border-fuchsia-600',
accentChipHover: 'hover:border-fuchsia-500/60 hover:text-fuchsia-400',
icon: Palette,
rateLabel: 'Rate',
portfolioLabel: 'Portfolio / Shop link',
skillSections: [
{ title: 'What You Make', entries: Object.entries(ARTIST_SERVICE_LABELS), required: true },
{ title: 'Medium', entries: Object.entries(ARTIST_MEDIUM_LABELS) },
{ title: 'Software & Tools', entries: Object.entries(ARTIST_TOOL_LABELS) },
],
},
},
graffiti: {
kind: 'craft',
seoTitle: 'Offer Graffiti / Murals - SkateSpots',
seoDescription: 'List your graffiti & mural work — pieces, murals, skatepark art and more.',
config: {
kind: SKATE_GRAFFITI_KIND,
selfTag: 'skategraffiti',
identifier: 'graffiti-profile',
heading: 'Graffiti / Mural Artist',
tagline: 'Pieces, murals, skatepark art — paint the scene',
noun: 'graffiti artist',
accentText: 'text-lime-400',
accentBg: 'bg-lime-500',
accentChip: 'bg-lime-500 text-black border-lime-500',
accentChipHover: 'hover:border-lime-500/60 hover:text-lime-400',
icon: SprayCan,
rateLabel: 'Rate',
portfolioLabel: 'Portfolio link (IG, site)',
skillSections: [
{ title: 'Style', entries: Object.entries(GRAFFITI_STYLE_LABELS), required: true },
{ title: 'Medium', entries: Object.entries(GRAFFITI_MEDIUM_LABELS) },
{ title: 'Work Context', entries: Object.entries(GRAFFITI_CONTEXT_LABELS) },
],
extraFields: [
{ tag: 'crew', label: 'Graffiti Crew', placeholder: 'Your writing crew (optional)', helper: 'Separate from your skate crew — the writers you paint with.' },
],
},
},
musician: {
kind: 'craft',
seoTitle: 'Add a Music Profile - SkateSpots',
seoDescription: 'Skate and make music? List your band or solo project — genres, instruments and links.',
config: {
kind: SKATE_MUSICIAN_KIND,
selfTag: 'skatemusician',
identifier: 'musician-profile',
heading: 'Musician / Band',
tagline: 'Skate and make music? Get booked for demos, jams & after-parties',
noun: 'musician',
accentText: 'text-sky-400',
accentBg: 'bg-sky-500',
accentChip: 'bg-sky-500 text-white border-sky-500',
accentChipHover: 'hover:border-sky-500/60 hover:text-sky-400',
icon: Music,
rateLabel: 'Booking Rate',
portfolioLabel: 'Website / EPK link',
skillSections: [
{ title: 'Genre', entries: Object.entries(MUSICIAN_GENRE_LABELS), required: true },
{ title: 'Instruments & Role', entries: Object.entries(MUSICIAN_INSTRUMENT_LABELS) },
],
extraFields: [
{ tag: 'act', label: 'Act Type', placeholder: 'solo / band / dj / producer' },
{ tag: 'spotify', label: 'Spotify', type: 'url', placeholder: 'https://open.spotify.com/…' },
{ tag: 'bandcamp', label: 'Bandcamp', type: 'url', placeholder: 'https://…bandcamp.com' },
{ tag: 'soundcloud', label: 'SoundCloud', type: 'url', placeholder: 'https://soundcloud.com/…' },
{ tag: 'youtube', label: 'YouTube', type: 'url', placeholder: 'https://youtube.com/…' },
],
},
},
judge: {
kind: 'craft',
seoTitle: 'Judge Skate Events - SkateSpots',
seoDescription: '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.',
config: {
kind: SKATE_JUDGE_KIND,
selfTag: 'skatejudge',
identifier: 'judge-profile',
heading: 'Skate Event Judge',
tagline: 'From flat-ground Game of S.K.A.T.E. to the World Destruction Tour — run it fair, run it credible',
noun: 'judge',
accentText: 'text-yellow-400',
accentBg: 'bg-yellow-500',
accentChip: 'bg-yellow-500 text-black border-yellow-500',
accentChipHover: 'hover:border-yellow-500/60 hover:text-yellow-400',
icon: Gavel,
rateLabel: 'Day Rate / Fee',
portfolioLabel: 'Results archive / profile link',
skillSections: [
{ title: 'Event Formats', entries: Object.entries(JUDGE_FORMAT_LABELS), required: true },
{ title: 'Specialty', entries: Object.entries(JUDGE_SPECIALTY_LABELS) },
{ title: 'Level / Circuit', entries: Object.entries(JUDGE_LEVEL_LABELS) },
],
extraFields: [
{ tag: 'events_judged', label: 'Events Judged', placeholder: 'e.g. 40+ contests, 3 World Tour stops', helper: 'Notable events, comps or tours you have officiated.' },
],
},
},
tattoo: {
kind: 'craft',
seoTitle: 'Become a Tattoo Artist - SkateSpots',
seoDescription: 'List your tattoo work for the skate scene — styles, flash, custom and more.',
config: {
kind: SKATE_TATTOO_KIND,
selfTag: 'skatetattoo',
identifier: 'tattoo-profile',
heading: 'Tattoo Artist',
tagline: 'Ink for the scene — flash, custom, fine line & more',
noun: 'tattoo artist',
accentText: 'text-rose-400',
accentBg: 'bg-rose-600',
accentChip: 'bg-rose-600 text-white border-rose-600',
accentChipHover: 'hover:border-rose-500/60 hover:text-rose-400',
icon: Brush,
rateLabel: 'Rate / Hourly',
portfolioLabel: 'Portfolio link (IG, site)',
skillSections: [
{ title: 'Styles', entries: Object.entries(TATTOO_STYLE_LABELS), required: true },
{ title: 'Method', entries: Object.entries(TATTOO_METHOD_LABELS) },
],
extraFields: [
{ tag: 'studio', label: 'Studio / Shop', placeholder: 'Studio name (optional)' },
{ tag: 'books_open', label: 'Books Open', type: 'toggle', helper: 'Are you currently taking bookings?' },
],
},
},
trainer: {
kind: 'craft',
seoTitle: 'Become a Trainer / Physio - SkateSpots',
seoDescription: 'Help skaters get strong and stay healthy — strength, mobility, injury prevention and recovery.',
config: {
kind: SKATE_TRAINER_KIND,
selfTag: 'skatetrainer',
identifier: 'trainer-profile',
heading: 'Trainer / Physio',
tagline: 'Strength, mobility, injury prevention & recovery for skaters',
noun: 'trainer',
accentText: 'text-emerald-400',
accentBg: 'bg-emerald-600',
accentChip: 'bg-emerald-600 text-white border-emerald-600',
accentChipHover: 'hover:border-emerald-500/60 hover:text-emerald-400',
icon: Dumbbell,
rateLabel: 'Rate / Session',
portfolioLabel: 'Profile / booking link',
skillSections: [
{ title: 'Focus', entries: Object.entries(TRAINER_FOCUS_LABELS), required: true },
{ title: 'Injuries You Work With', entries: Object.entries(TRAINER_INJURY_LABELS) },
{ title: 'Credentials & Setting', entries: Object.entries(TRAINER_CREDENTIAL_LABELS) },
],
extraFields: [
{ tag: 'credentials', label: 'Certifications', placeholder: 'e.g. BSc Physiotherapy, CSCS…', helper: 'List real qualifications — skaters trust verifiable credentials.' },
{ tag: 'remote', label: 'Remote / Online', type: 'toggle', helper: 'Do you offer online coaching or consultations?' },
],
},
},
};
export function getBecomeRole(slug: string | undefined): BecomeRole | undefined {
if (!slug) return undefined;
return BECOME_ROLES[slug.toLowerCase()];
}

View file

@ -1,38 +0,0 @@
import { useSeoMeta } from '@unhead/react';
import { Palette } from 'lucide-react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import {
SKATE_ARTIST_KIND, ARTIST_SERVICE_LABELS, ARTIST_MEDIUM_LABELS, ARTIST_TOOL_LABELS,
} from '@/lib/types';
export default function BecomeArtistPage() {
useSeoMeta({
title: 'Become a Designer / Artist - SkateSpots',
description: 'List your skate design & art — board graphics, logos, posters, merch and more.',
});
return (
<CraftProfileForm
config={{
kind: SKATE_ARTIST_KIND,
selfTag: 'skateartist',
identifier: 'artist-profile',
heading: 'Designer / Artist',
tagline: 'Board graphics, logos, posters, merch — put your art on the scene',
noun: 'artist',
accentText: 'text-fuchsia-400',
accentBg: 'bg-fuchsia-600',
accentChip: 'bg-fuchsia-600 text-white border-fuchsia-600',
accentChipHover: 'hover:border-fuchsia-500/60 hover:text-fuchsia-400',
icon: Palette,
rateLabel: 'Rate',
portfolioLabel: 'Portfolio / Shop link',
skillSections: [
{ title: 'What You Make', entries: Object.entries(ARTIST_SERVICE_LABELS), required: true },
{ title: 'Medium', entries: Object.entries(ARTIST_MEDIUM_LABELS) },
{ title: 'Software & Tools', entries: Object.entries(ARTIST_TOOL_LABELS) },
],
}}
/>
);
}

View file

@ -1,41 +0,0 @@
import { useSeoMeta } from '@unhead/react';
import { SprayCan } from 'lucide-react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import {
SKATE_GRAFFITI_KIND, GRAFFITI_STYLE_LABELS, GRAFFITI_MEDIUM_LABELS, GRAFFITI_CONTEXT_LABELS,
} from '@/lib/types';
export default function BecomeGraffitiPage() {
useSeoMeta({
title: 'Offer Graffiti / Murals - SkateSpots',
description: 'List your graffiti & mural work — pieces, murals, skatepark art and more.',
});
return (
<CraftProfileForm
config={{
kind: SKATE_GRAFFITI_KIND,
selfTag: 'skategraffiti',
identifier: 'graffiti-profile',
heading: 'Graffiti / Mural Artist',
tagline: 'Pieces, murals, skatepark art — paint the scene',
noun: 'graffiti artist',
accentText: 'text-lime-400',
accentBg: 'bg-lime-500',
accentChip: 'bg-lime-500 text-black border-lime-500',
accentChipHover: 'hover:border-lime-500/60 hover:text-lime-400',
icon: SprayCan,
rateLabel: 'Rate',
portfolioLabel: 'Portfolio link (IG, site)',
skillSections: [
{ title: 'Style', entries: Object.entries(GRAFFITI_STYLE_LABELS), required: true },
{ title: 'Medium', entries: Object.entries(GRAFFITI_MEDIUM_LABELS) },
{ title: 'Work Context', entries: Object.entries(GRAFFITI_CONTEXT_LABELS) },
],
extraFields: [
{ tag: 'crew', label: 'Graffiti Crew', placeholder: 'Your writing crew (optional)', helper: 'Separate from your skate crew — the writers you paint with.' },
],
}}
/>
);
}

View file

@ -1,50 +0,0 @@
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 (
<CraftProfileForm
config={{
kind: SKATE_JUDGE_KIND,
selfTag: 'skatejudge',
identifier: 'judge-profile',
heading: 'Skate Event Judge',
tagline: 'From flat-ground Game of S.K.A.T.E. to the World Destruction Tour — run it fair, run it credible',
noun: 'judge',
accentText: 'text-yellow-400',
accentBg: 'bg-yellow-500',
accentChip: 'bg-yellow-500 text-black border-yellow-500',
accentChipHover: 'hover:border-yellow-500/60 hover:text-yellow-400',
icon: Gavel,
rateLabel: 'Day Rate / Fee',
portfolioLabel: 'Results archive / profile link',
skillSections: [
{ title: 'Event Formats', entries: Object.entries(JUDGE_FORMAT_LABELS), required: true },
{ title: 'Specialty', entries: Object.entries(JUDGE_SPECIALTY_LABELS) },
{ title: 'Level / Circuit', entries: Object.entries(JUDGE_LEVEL_LABELS) },
],
extraFields: [
{
tag: 'events_judged',
label: 'Events Judged',
placeholder: 'e.g. 40+ contests, 3 World Tour stops',
helper: 'Notable events, comps or tours you have officiated.',
},
],
}}
/>
);
}

View file

@ -1,42 +0,0 @@
import { useSeoMeta } from '@unhead/react';
import { Music } from 'lucide-react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import { SKATE_MUSICIAN_KIND, MUSICIAN_GENRE_LABELS, MUSICIAN_INSTRUMENT_LABELS } from '@/lib/types';
export default function BecomeMusicianPage() {
useSeoMeta({
title: 'Add a Music Profile - SkateSpots',
description: 'Skate and make music? List your band or solo project — genres, instruments and links.',
});
return (
<CraftProfileForm
config={{
kind: SKATE_MUSICIAN_KIND,
selfTag: 'skatemusician',
identifier: 'musician-profile',
heading: 'Musician / Band',
tagline: 'Skate and make music? Get booked for demos, jams & after-parties',
noun: 'musician',
accentText: 'text-sky-400',
accentBg: 'bg-sky-500',
accentChip: 'bg-sky-500 text-white border-sky-500',
accentChipHover: 'hover:border-sky-500/60 hover:text-sky-400',
icon: Music,
rateLabel: 'Booking Rate',
portfolioLabel: 'Website / EPK link',
skillSections: [
{ title: 'Genre', entries: Object.entries(MUSICIAN_GENRE_LABELS), required: true },
{ title: 'Instruments & Role', entries: Object.entries(MUSICIAN_INSTRUMENT_LABELS) },
],
extraFields: [
{ tag: 'act', label: 'Act Type', placeholder: 'solo / band / dj / producer' },
{ tag: 'spotify', label: 'Spotify', type: 'url', placeholder: 'https://open.spotify.com/…' },
{ tag: 'bandcamp', label: 'Bandcamp', type: 'url', placeholder: 'https://…bandcamp.com' },
{ tag: 'soundcloud', label: 'SoundCloud', type: 'url', placeholder: 'https://soundcloud.com/…' },
{ tag: 'youtube', label: 'YouTube', type: 'url', placeholder: 'https://youtube.com/…' },
],
}}
/>
);
}

View file

@ -0,0 +1,57 @@
import { Suspense } from 'react';
import { useParams } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import { getBecomeRole } from '@/lib/becomeRoles';
import NotFound from '@/pages/NotFound';
/**
* Single entry point for every "become a <role>" flow (/become/:role).
*
* - Craft roles render the shared <CraftProfileForm> from their data config.
* - Bespoke roles render their own lazy page component.
* - Unknown roles fall through to the 404 page.
*
* This replaces 12 hardcoded routes + 12 component imports in the router.
*/
function PageLoader() {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ff0040] border-t-transparent rounded-full animate-spin" />
</div>
);
}
export default function BecomeRolePage() {
const { role: slug } = useParams<{ role: string }>();
const role = getBecomeRole(slug);
if (!role) {
return <NotFound />;
}
if (role.kind === 'craft') {
return <CraftRole role={role} />;
}
const Component = role.component;
return (
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
);
}
function CraftRole({
role,
}: {
role: Extract<ReturnType<typeof getBecomeRole>, { kind: 'craft' }>;
}) {
useSeoMeta({
title: role.seoTitle,
description: role.seoDescription,
});
return <CraftProfileForm config={role.config} />;
}

View file

@ -1,39 +0,0 @@
import { useSeoMeta } from '@unhead/react';
import { Brush } from 'lucide-react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import { SKATE_TATTOO_KIND, TATTOO_STYLE_LABELS, TATTOO_METHOD_LABELS } from '@/lib/types';
export default function BecomeTattooPage() {
useSeoMeta({
title: 'Become a Tattoo Artist - SkateSpots',
description: 'List your tattoo work for the skate scene — styles, flash, custom and more.',
});
return (
<CraftProfileForm
config={{
kind: SKATE_TATTOO_KIND,
selfTag: 'skatetattoo',
identifier: 'tattoo-profile',
heading: 'Tattoo Artist',
tagline: 'Ink for the scene — flash, custom, fine line & more',
noun: 'tattoo artist',
accentText: 'text-rose-400',
accentBg: 'bg-rose-600',
accentChip: 'bg-rose-600 text-white border-rose-600',
accentChipHover: 'hover:border-rose-500/60 hover:text-rose-400',
icon: Brush,
rateLabel: 'Rate / Hourly',
portfolioLabel: 'Portfolio link (IG, site)',
skillSections: [
{ title: 'Styles', entries: Object.entries(TATTOO_STYLE_LABELS), required: true },
{ title: 'Method', entries: Object.entries(TATTOO_METHOD_LABELS) },
],
extraFields: [
{ tag: 'studio', label: 'Studio / Shop', placeholder: 'Studio name (optional)' },
{ tag: 'books_open', label: 'Books Open', type: 'toggle', helper: 'Are you currently taking bookings?' },
],
}}
/>
);
}

View file

@ -1,42 +0,0 @@
import { useSeoMeta } from '@unhead/react';
import { Dumbbell } from 'lucide-react';
import { CraftProfileForm } from '@/components/CraftProfileForm';
import {
SKATE_TRAINER_KIND, TRAINER_FOCUS_LABELS, TRAINER_INJURY_LABELS, TRAINER_CREDENTIAL_LABELS,
} from '@/lib/types';
export default function BecomeTrainerPage() {
useSeoMeta({
title: 'Become a Trainer / Physio - SkateSpots',
description: 'Help skaters get strong and stay healthy — strength, mobility, injury prevention and recovery.',
});
return (
<CraftProfileForm
config={{
kind: SKATE_TRAINER_KIND,
selfTag: 'skatetrainer',
identifier: 'trainer-profile',
heading: 'Trainer / Physio',
tagline: 'Strength, mobility, injury prevention & recovery for skaters',
noun: 'trainer',
accentText: 'text-emerald-400',
accentBg: 'bg-emerald-600',
accentChip: 'bg-emerald-600 text-white border-emerald-600',
accentChipHover: 'hover:border-emerald-500/60 hover:text-emerald-400',
icon: Dumbbell,
rateLabel: 'Rate / Session',
portfolioLabel: 'Profile / booking link',
skillSections: [
{ title: 'Focus', entries: Object.entries(TRAINER_FOCUS_LABELS), required: true },
{ title: 'Injuries You Work With', entries: Object.entries(TRAINER_INJURY_LABELS) },
{ title: 'Credentials & Setting', entries: Object.entries(TRAINER_CREDENTIAL_LABELS) },
],
extraFields: [
{ tag: 'credentials', label: 'Certifications', placeholder: 'e.g. BSc Physiotherapy, CSCS…', helper: 'List real qualifications — skaters trust verifiable credentials.' },
{ tag: 'remote', label: 'Remote / Online', type: 'toggle', helper: 'Do you offer online coaching or consultations?' },
],
}}
/>
);
}

View file

@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { CrewRepBadge, CrewActivityPulse } from '@/components/CrewBadge'; import { CrewRepBadge, CrewActivityPulse } from '@/components/CrewBadge';
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
import { useCrews } from '@/hooks/useCrews'; import { useCrews } from '@/hooks/useCrews';
import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity'; import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity';
import { useGeolocation } from '@/hooks/useGeolocation'; import { useGeolocation } from '@/hooks/useGeolocation';
@ -358,48 +359,43 @@ function EmptyState({
}) { }) {
if (query) { if (query) {
return ( return (
<div className="flex flex-col items-center justify-center px-4 py-20 text-center"> <SharedEmptyState
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5"> icon={Users}
<Users className="h-8 w-8 text-gray-600" /> title="No crews match"
</div> hint="Try a different search."
<h3 className="mb-2 text-lg font-black text-white">No crews match</h3> />
<p className="max-w-xs text-sm text-gray-500">Try a different search.</p>
</div>
); );
} }
// Rich empty state — never feels dead. Prompts to claim the local scene. // Rich empty state — never feels dead. Prompts to claim the local scene.
return ( return (
<Card className="border-2 border-dashed border-teal-500/30 bg-gradient-to-br from-teal-500/[0.06] to-transparent"> <Card className="border-2 border-dashed border-teal-500/30 bg-gradient-to-br from-teal-500/[0.06] to-transparent">
<CardContent className="px-6 py-12 text-center"> <CardContent className="p-0">
<div className="mx-auto max-w-sm space-y-5"> <SharedEmptyState
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-teal-500/10 text-4xl"> emoji="🛹"
🛹 title="Claim your local scene"
</div> hint="No crews planted here yet. Be the first — start your crew, link your home spots, and put your scene on the map for everyone to find."
<div className="space-y-1.5"> actions={[
<h3 className="text-xl font-black uppercase text-white">Claim your local scene</h3> {
<p className="text-sm text-gray-400"> label: 'Create Crew',
No crews planted here yet. Be the first — start your crew, link your home spots, to: '/create-crew',
and put your scene on the map for everyone to find. icon: Plus,
</p> className: 'bg-teal-500 text-black hover:bg-teal-600',
</div> },
<div className="flex flex-col gap-2 sm:flex-row sm:justify-center"> ...(!hasLocation
<Link to="/create-crew"> ? [
<Button className="w-full gap-2 bg-teal-500 font-black uppercase text-black hover:bg-teal-600 sm:w-auto"> {
<Plus className="h-4 w-4" /> Create Crew label: 'Find crews near me',
</Button> onClick: onNearMe,
</Link> icon: Navigation,
{!hasLocation && ( variant: 'secondary' as const,
<Button className:
variant="outline" 'border-orange-500/40 text-orange-300 hover:bg-orange-500/10',
onClick={onNearMe} },
className="w-full gap-2 border-orange-500/40 font-bold text-orange-300 hover:bg-orange-500/10 sm:w-auto" ]
> : []),
<Navigation className="h-4 w-4" /> Find crews near me ]}
</Button> />
)}
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
); );

View file

@ -44,7 +44,7 @@ import {
LayoutGrid, LayoutGrid,
CalendarDays, CalendarDays,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { EmptyState as SharedEmptyState, type EmptyStateAction } from '@/components/EmptyState';
// ─── Accent colours ─────────────────────────────────────────────────────────── // ─── Accent colours ───────────────────────────────────────────────────────────
@ -777,33 +777,21 @@ function EmptyState({
onClear?: () => void; onClear?: () => void;
hasFilters: boolean; hasFilters: boolean;
}) { }) {
const actions: EmptyStateAction[] =
hasFilters && onClear
? [{ label: 'Clear Filters', onClick: onClear, icon: X, variant: 'secondary' }]
: cta && to
? [{ label: cta, to, icon: Plus, className: ctaClass }]
: [];
return ( return (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center"> <SharedEmptyState
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4"> icon={Icon}
<Icon className="h-8 w-8 text-gray-600" /> title={title}
</div> hint={body}
<h3 className="text-lg font-black mb-2 text-white">{title}</h3> actions={actions}
<p className="text-gray-500 text-sm max-w-xs mb-6">{body}</p> footer={{ label: 'Have questions? Check the FAQ →', to: '/faq' }}
{hasFilters && onClear ? ( />
<button
onClick={onClear}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl font-black text-sm uppercase tracking-wide text-white bg-white/10 hover:bg-white/15 transition-all active:scale-95"
>
<X className="h-4 w-4" />
Clear Filters
</button>
) : cta && to ? (
<Link to={to}>
<button className={cn('flex items-center gap-2 px-5 py-2.5 rounded-xl font-black text-sm uppercase tracking-wide text-white transition-all active:scale-95', ctaClass)}>
<Plus className="h-4 w-4" />
{cta}
</button>
</Link>
) : null}
<Link to="/faq" className="mt-5 text-xs text-gray-600 hover:text-gray-400 underline underline-offset-2 transition-colors">
Have questions? Check the FAQ →
</Link>
</div>
); );
} }

View file

@ -314,13 +314,13 @@ function TheLabSection() {
{t('lab.noCoaches')} {t('lab.noCoaches')}
</p> </p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link to="/become-coach"> <Link to="/become/coach">
<Button className="gap-2 bg-red-500 hover:bg-red-600 font-bold uppercase"> <Button className="gap-2 bg-red-500 hover:bg-red-600 font-bold uppercase">
<GraduationCap className="h-4 w-4" /> <GraduationCap className="h-4 w-4" />
{t('home.becomeCoach')} {t('home.becomeCoach')}
</Button> </Button>
</Link> </Link>
<Link to="/become-filmer"> <Link to="/become/filmer">
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 font-bold uppercase"> <Button className="gap-2 bg-orange-500 hover:bg-orange-600 font-bold uppercase">
<Video className="h-4 w-4" /> <Video className="h-4 w-4" />
{t('home.becomeFilmer')} {t('home.becomeFilmer')}

View file

@ -311,17 +311,17 @@ function OfferFAB() {
}, [open]); }, [open]);
const items = [ const items = [
{ to: '/become-coach', label: 'Offer Coaching', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', icon: GraduationCap }, { to: '/become/coach', label: 'Offer Coaching', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', icon: GraduationCap },
{ to: '/become-filmer', label: 'Offer Filming', color: 'bg-orange-500 hover:bg-orange-600', icon: Video }, { to: '/become/filmer', label: 'Offer Filming', color: 'bg-orange-500 hover:bg-orange-600', icon: Video },
{ to: '/become-photographer', label: 'Offer Photography', color: 'bg-indigo-600 hover:bg-indigo-700', icon: Camera }, { to: '/become/photographer', label: 'Offer Photography', color: 'bg-indigo-600 hover:bg-indigo-700', icon: Camera },
{ to: '/become-editor', label: 'Offer Editing', color: 'bg-purple-600 hover:bg-purple-700', icon: Scissors }, { to: '/become/editor', label: 'Offer Editing', color: 'bg-purple-600 hover:bg-purple-700', icon: Scissors },
{ to: '/become-builder', label: 'Offer Building', color: 'bg-amber-500 hover:bg-amber-600 text-black', icon: HardHat }, { to: '/become/builder', label: 'Offer Building', color: 'bg-amber-500 hover:bg-amber-600 text-black', icon: HardHat },
{ to: '/become-artist', label: 'Offer Design / Art', color: 'bg-fuchsia-600 hover:bg-fuchsia-700', icon: Palette }, { to: '/become/artist', label: 'Offer Design / Art', color: 'bg-fuchsia-600 hover:bg-fuchsia-700', icon: Palette },
{ to: '/become-graffiti', label: 'Offer Graffiti', color: 'bg-lime-500 hover:bg-lime-600 text-black', icon: SprayCan }, { to: '/become/graffiti', label: 'Offer Graffiti', color: 'bg-lime-500 hover:bg-lime-600 text-black', icon: SprayCan },
{ to: '/become-tattoo', label: 'Offer Tattoo', color: 'bg-rose-600 hover:bg-rose-700', icon: Brush }, { 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/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: '/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 },
]; ];
@ -1007,7 +1007,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No coaches yet', title: 'No coaches yet',
body: 'Be the first to offer skateboarding coaching in your area.', body: 'Be the first to offer skateboarding coaching in your area.',
cta: 'Offer Coaching', cta: 'Offer Coaching',
to: '/become-coach', to: '/become/coach',
color: 'bg-[#ff0040] hover:bg-[#ff0040]/90', color: 'bg-[#ff0040] hover:bg-[#ff0040]/90',
}, },
filming: { filming: {
@ -1015,7 +1015,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No filmers yet', title: 'No filmers yet',
body: 'Be the first to offer skate filming — clips, full parts, contest coverage and more.', body: 'Be the first to offer skate filming — clips, full parts, contest coverage and more.',
cta: 'Offer Filming', cta: 'Offer Filming',
to: '/become-filmer', to: '/become/filmer',
color: 'bg-orange-500 hover:bg-orange-600', color: 'bg-orange-500 hover:bg-orange-600',
}, },
photography: { photography: {
@ -1023,7 +1023,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No photographers yet', title: 'No photographers yet',
body: 'Be the first to offer skate photography in your area — action shots, sequences, editorial and more.', body: 'Be the first to offer skate photography in your area — action shots, sequences, editorial and more.',
cta: 'Offer Photography', cta: 'Offer Photography',
to: '/become-photographer', to: '/become/photographer',
color: 'bg-indigo-600 hover:bg-indigo-700', color: 'bg-indigo-600 hover:bg-indigo-700',
}, },
editing: { editing: {
@ -1031,7 +1031,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No video editors yet', title: 'No video editors yet',
body: 'Be the first to offer skate video editing services.', body: 'Be the first to offer skate video editing services.',
cta: 'Offer Editing', cta: 'Offer Editing',
to: '/become-editor', to: '/become/editor',
color: 'bg-purple-600 hover:bg-purple-700', color: 'bg-purple-600 hover:bg-purple-700',
}, },
building: { building: {
@ -1039,7 +1039,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No builders yet', title: 'No builders yet',
body: 'Be the first to list your ramp, park or DIY building — concrete, wood, rails and more.', body: 'Be the first to list your ramp, park or DIY building — concrete, wood, rails and more.',
cta: 'Offer Building', cta: 'Offer Building',
to: '/become-builder', to: '/become/builder',
color: 'bg-amber-500 hover:bg-amber-600 text-black', color: 'bg-amber-500 hover:bg-amber-600 text-black',
}, },
art: { art: {
@ -1047,7 +1047,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No artists yet', title: 'No artists yet',
body: 'Be the first to offer board graphics, logos, posters and merch for the scene.', body: 'Be the first to offer board graphics, logos, posters and merch for the scene.',
cta: 'Offer Design / Art', cta: 'Offer Design / Art',
to: '/become-artist', to: '/become/artist',
color: 'bg-fuchsia-600 hover:bg-fuchsia-700', color: 'bg-fuchsia-600 hover:bg-fuchsia-700',
}, },
graffiti: { graffiti: {
@ -1055,7 +1055,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No graffiti artists yet', title: 'No graffiti artists yet',
body: 'Be the first to offer pieces, murals and skatepark art in your area.', body: 'Be the first to offer pieces, murals and skatepark art in your area.',
cta: 'Offer Graffiti', cta: 'Offer Graffiti',
to: '/become-graffiti', to: '/become/graffiti',
color: 'bg-lime-500 hover:bg-lime-600 text-black', color: 'bg-lime-500 hover:bg-lime-600 text-black',
}, },
tattoo: { tattoo: {
@ -1063,7 +1063,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No tattoo artists yet', title: 'No tattoo artists yet',
body: 'Be the first skate-culture tattoo artist to list — flash, custom, fine line and more.', body: 'Be the first skate-culture tattoo artist to list — flash, custom, fine line and more.',
cta: 'Offer Tattoo', cta: 'Offer Tattoo',
to: '/become-tattoo', to: '/become/tattoo',
color: 'bg-rose-600 hover:bg-rose-700', color: 'bg-rose-600 hover:bg-rose-700',
}, },
training: { training: {
@ -1071,7 +1071,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No trainers yet', title: 'No trainers yet',
body: 'Be the first trainer or physio to help skaters get strong and stay healthy.', body: 'Be the first trainer or physio to help skaters get strong and stay healthy.',
cta: 'Offer Training / Physio', cta: 'Offer Training / Physio',
to: '/become-trainer', to: '/become/trainer',
color: 'bg-emerald-600 hover:bg-emerald-700', color: 'bg-emerald-600 hover:bg-emerald-700',
}, },
music: { music: {
@ -1079,7 +1079,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No musicians yet', title: 'No musicians yet',
body: 'Skate and make music? Add your band or solo project — genres, instruments and links.', body: 'Skate and make music? Add your band or solo project — genres, instruments and links.',
cta: 'Add Music Profile', cta: 'Add Music Profile',
to: '/become-musician', to: '/become/musician',
color: 'bg-sky-500 hover:bg-sky-600', color: 'bg-sky-500 hover:bg-sky-600',
}, },
judging: { judging: {
@ -1087,7 +1087,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
title: 'No judges yet', 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.', 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', cta: 'Judge Events',
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',
}, },
}; };

View file

@ -430,7 +430,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
) : null} ) : null}
{isOwnProfile && ( {isOwnProfile && (
<Link <Link
to="/become-skater" to="/become/skater"
className="text-[10px] font-black uppercase tracking-wider text-[#ff0040] hover:underline flex items-center gap-1" className="text-[10px] font-black uppercase tracking-wider text-[#ff0040] hover:underline flex items-center gap-1"
> >
<Pencil className="h-3 w-3" /> Edit <Pencil className="h-3 w-3" /> Edit
@ -516,7 +516,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
) : ( ) : (
isOwnProfile && ( isOwnProfile && (
<Link <Link
to="/become-skater" to="/become/skater"
className="group flex items-center gap-3 rounded-xl border-2 border-dashed border-[#ff0040]/30 bg-[#ff0040]/5 p-4 hover:border-[#ff0040]/60 hover:bg-[#ff0040]/10 transition-all" className="group flex items-center gap-3 rounded-xl border-2 border-dashed border-[#ff0040]/30 bg-[#ff0040]/5 p-4 hover:border-[#ff0040]/60 hover:bg-[#ff0040]/10 transition-all"
> >
<div className="flex-shrink-0 w-11 h-11 rounded-xl bg-[#ff0040]/15 flex items-center justify-center group-hover:bg-[#ff0040]/25 transition-colors"> <div className="flex-shrink-0 w-11 h-11 rounded-xl bg-[#ff0040]/15 flex items-center justify-center group-hover:bg-[#ff0040]/25 transition-colors">

View file

@ -200,14 +200,14 @@ export default function SkatersPage() {
<div className="flex flex-wrap items-center gap-3 mt-6"> <div className="flex flex-wrap items-center gap-3 mt-6">
{!myProfile ? ( {!myProfile ? (
<Link to="/become-skater"> <Link to="/become/skater">
<Button className="h-11 bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2"> <Button className="h-11 bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
Put me on the map Put me on the map
</Button> </Button>
</Link> </Link>
) : ( ) : (
<Link to="/become-skater"> <Link to="/become/skater">
<Button variant="outline" className="h-11 border-[#ff0040]/50 text-[#ff0040] hover:bg-[#ff0040]/10 font-bold gap-2"> <Button variant="outline" className="h-11 border-[#ff0040]/50 text-[#ff0040] hover:bg-[#ff0040]/10 font-bold gap-2">
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
Edit my skater profile Edit my skater profile
@ -317,7 +317,7 @@ export default function SkatersPage() {
<p className="text-gray-400 max-w-sm mx-auto mb-6"> <p className="text-gray-400 max-w-sm mx-auto mb-6">
Be the first to join the directory and put SkateSpots on the map. Be the first to join the directory and put SkateSpots on the map.
</p> </p>
<Link to="/become-skater"> <Link to="/become/skater">
<Button className="bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2"> <Button className="bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2">
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
Put me on the map Put me on the map

View file

@ -720,13 +720,13 @@ export default function SpotDetailPage() {
💡 Offer your services at this spot 💡 Offer your services at this spot
</p> </p>
<div className="flex gap-2 justify-center"> <div className="flex gap-2 justify-center">
<Link to="/become-coach"> <Link to="/become/coach">
<Button size="sm" variant="outline" className="gap-2 border-lime-500/50 text-lime-400 hover:bg-lime-500/10"> <Button size="sm" variant="outline" className="gap-2 border-lime-500/50 text-lime-400 hover:bg-lime-500/10">
<GraduationCap className="h-4 w-4" /> <GraduationCap className="h-4 w-4" />
Coach Here Coach Here
</Button> </Button>
</Link> </Link>
<Link to="/become-filmer"> <Link to="/become/filmer">
<Button size="sm" variant="outline" className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10"> <Button size="sm" variant="outline" className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10">
<Video className="h-4 w-4" /> <Video className="h-4 w-4" />
Film Here Film Here