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:
parent
e7e492c568
commit
16e943d4ef
17 changed files with 385 additions and 373 deletions
|
|
@ -1,6 +1,7 @@
|
|||
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 { BECOME_ROLES } from "./lib/becomeRoles";
|
||||
import { DeepLinkHandler } from "./components/DeepLinkHandler";
|
||||
import { RouteErrorBoundary } from "./components/RouteErrorBoundary";
|
||||
|
||||
|
|
@ -28,18 +29,9 @@ const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage"));
|
|||
const EventDetailPage = lazy(() => import("./pages/EventDetailPage"));
|
||||
const OnboardingPage = lazy(() => import("./pages/OnboardingPage"));
|
||||
const PyramidPage = lazy(() => import("./pages/PyramidPage"));
|
||||
const BecomeCoachPage = lazy(() => import("./pages/BecomeCoachPage"));
|
||||
const BecomeFilmerPage = lazy(() => import("./pages/BecomeFilmerPage"));
|
||||
const BecomeEditorPage = lazy(() => import("./pages/BecomeEditorPage"));
|
||||
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"));
|
||||
// All "become a <role>" flows now share a single param route backed by a role
|
||||
// registry (see src/lib/becomeRoles.tsx) instead of 12 separate route lines.
|
||||
const BecomeRolePage = lazy(() => import("./pages/BecomeRolePage"));
|
||||
const SkatersPage = lazy(() => import("./pages/SkatersPage"));
|
||||
const CrewsPage = lazy(() => import("./pages/CrewsPage"));
|
||||
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() {
|
||||
return (
|
||||
<HashRouter>
|
||||
|
|
@ -100,19 +96,16 @@ export function AppRouter() {
|
|||
<Route path="/lab" element={<LabPage />} />
|
||||
{/* Legacy route redirect */}
|
||||
<Route path="/coaches" element={<LabPage />} />
|
||||
<Route path="/become-coach" element={<BecomeCoachPage />} />
|
||||
<Route path="/become-filmer" element={<BecomeFilmerPage />} />
|
||||
<Route path="/become-editor" element={<BecomeEditorPage />} />
|
||||
<Route path="/become-photographer" element={<BecomePhotographerPage />} />
|
||||
<Route path="/become-builder" element={<BecomeBuilderPage />} />
|
||||
<Route path="/become-artist" element={<BecomeArtistPage />} />
|
||||
<Route path="/become-graffiti" element={<BecomeGraffitiPage />} />
|
||||
<Route path="/become-tattoo" element={<BecomeTattooPage />} />
|
||||
<Route path="/become-trainer" element={<BecomeTrainerPage />} />
|
||||
<Route path="/become-musician" element={<BecomeMusicianPage />} />
|
||||
<Route path="/become-judge" element={<BecomeJudgePage />} />
|
||||
{/* Unified role route + legacy /become-<slug> redirects */}
|
||||
<Route path="/become/:role" element={<BecomeRolePage />} />
|
||||
{BECOME_SLUGS.map((slug) => (
|
||||
<Route
|
||||
key={slug}
|
||||
path={`/become-${slug}`}
|
||||
element={<Navigate to={`/become/${slug}`} replace />}
|
||||
/>
|
||||
))}
|
||||
<Route path="/skaters" element={<SkatersPage />} />
|
||||
<Route path="/become-skater" element={<BecomeSkaterPage />} />
|
||||
<Route path="/crews" element={<CrewsPage />} />
|
||||
<Route path="/create-crew" element={<CreateCrewPage />} />
|
||||
<Route path="/edit-crew/:pubkey/:identifier" element={<EditCrewPage />} />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function SkaterDirectoryCard() {
|
|||
if (!skater) {
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<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 (
|
||||
<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"
|
||||
>
|
||||
<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
230
src/lib/becomeRoles.tsx
Normal 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()];
|
||||
}
|
||||
|
|
@ -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) },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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.' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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.',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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/…' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
57
src/pages/BecomeRolePage.tsx
Normal file
57
src/pages/BecomeRolePage.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -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?' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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?' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { CrewRepBadge, CrewActivityPulse } from '@/components/CrewBadge';
|
||||
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
|
||||
import { useCrews } from '@/hooks/useCrews';
|
||||
import { useCrewsActivity, crewCoordinate } from '@/hooks/useCrewActivity';
|
||||
import { useGeolocation } from '@/hooks/useGeolocation';
|
||||
|
|
@ -358,48 +359,43 @@ function EmptyState({
|
|||
}) {
|
||||
if (query) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4 py-20 text-center">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5">
|
||||
<Users className="h-8 w-8 text-gray-600" />
|
||||
</div>
|
||||
<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>
|
||||
<SharedEmptyState
|
||||
icon={Users}
|
||||
title="No crews match"
|
||||
hint="Try a different search."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Rich empty state — never feels dead. Prompts to claim the local scene.
|
||||
return (
|
||||
<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">
|
||||
<div className="mx-auto max-w-sm space-y-5">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-teal-500/10 text-4xl">
|
||||
🛹
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h3 className="text-xl font-black uppercase text-white">Claim your local scene</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:justify-center">
|
||||
<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
|
||||
</Button>
|
||||
</Link>
|
||||
{!hasLocation && (
|
||||
<Button
|
||||
variant="outline"
|
||||
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 className="p-0">
|
||||
<SharedEmptyState
|
||||
emoji="🛹"
|
||||
title="Claim your local scene"
|
||||
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."
|
||||
actions={[
|
||||
{
|
||||
label: 'Create Crew',
|
||||
to: '/create-crew',
|
||||
icon: Plus,
|
||||
className: 'bg-teal-500 text-black hover:bg-teal-600',
|
||||
},
|
||||
...(!hasLocation
|
||||
? [
|
||||
{
|
||||
label: 'Find crews near me',
|
||||
onClick: onNearMe,
|
||||
icon: Navigation,
|
||||
variant: 'secondary' as const,
|
||||
className:
|
||||
'border-orange-500/40 text-orange-300 hover:bg-orange-500/10',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import {
|
|||
LayoutGrid,
|
||||
CalendarDays,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EmptyState as SharedEmptyState, type EmptyStateAction } from '@/components/EmptyState';
|
||||
|
||||
// ─── Accent colours ───────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -777,33 +777,21 @@ function EmptyState({
|
|||
onClear?: () => void;
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4">
|
||||
<Icon className="h-8 w-8 text-gray-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-black mb-2 text-white">{title}</h3>
|
||||
<p className="text-gray-500 text-sm max-w-xs mb-6">{body}</p>
|
||||
{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>
|
||||
<SharedEmptyState
|
||||
icon={Icon}
|
||||
title={title}
|
||||
hint={body}
|
||||
actions={actions}
|
||||
footer={{ label: 'Have questions? Check the FAQ →', to: '/faq' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -314,13 +314,13 @@ function TheLabSection() {
|
|||
{t('lab.noCoaches')}
|
||||
</p>
|
||||
<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">
|
||||
<GraduationCap className="h-4 w-4" />
|
||||
{t('home.becomeCoach')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/become-filmer">
|
||||
<Link to="/become/filmer">
|
||||
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 font-bold uppercase">
|
||||
<Video className="h-4 w-4" />
|
||||
{t('home.becomeFilmer')}
|
||||
|
|
|
|||
|
|
@ -311,17 +311,17 @@ function OfferFAB() {
|
|||
}, [open]);
|
||||
|
||||
const items = [
|
||||
{ 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-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-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-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-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: '/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/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/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/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/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 },
|
||||
];
|
||||
|
||||
|
|
@ -1007,7 +1007,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No coaches yet',
|
||||
body: 'Be the first to offer skateboarding coaching in your area.',
|
||||
cta: 'Offer Coaching',
|
||||
to: '/become-coach',
|
||||
to: '/become/coach',
|
||||
color: 'bg-[#ff0040] hover:bg-[#ff0040]/90',
|
||||
},
|
||||
filming: {
|
||||
|
|
@ -1015,7 +1015,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No filmers yet',
|
||||
body: 'Be the first to offer skate filming — clips, full parts, contest coverage and more.',
|
||||
cta: 'Offer Filming',
|
||||
to: '/become-filmer',
|
||||
to: '/become/filmer',
|
||||
color: 'bg-orange-500 hover:bg-orange-600',
|
||||
},
|
||||
photography: {
|
||||
|
|
@ -1023,7 +1023,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No photographers yet',
|
||||
body: 'Be the first to offer skate photography in your area — action shots, sequences, editorial and more.',
|
||||
cta: 'Offer Photography',
|
||||
to: '/become-photographer',
|
||||
to: '/become/photographer',
|
||||
color: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
},
|
||||
editing: {
|
||||
|
|
@ -1031,7 +1031,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No video editors yet',
|
||||
body: 'Be the first to offer skate video editing services.',
|
||||
cta: 'Offer Editing',
|
||||
to: '/become-editor',
|
||||
to: '/become/editor',
|
||||
color: 'bg-purple-600 hover:bg-purple-700',
|
||||
},
|
||||
building: {
|
||||
|
|
@ -1039,7 +1039,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No builders yet',
|
||||
body: 'Be the first to list your ramp, park or DIY building — concrete, wood, rails and more.',
|
||||
cta: 'Offer Building',
|
||||
to: '/become-builder',
|
||||
to: '/become/builder',
|
||||
color: 'bg-amber-500 hover:bg-amber-600 text-black',
|
||||
},
|
||||
art: {
|
||||
|
|
@ -1047,7 +1047,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No artists yet',
|
||||
body: 'Be the first to offer board graphics, logos, posters and merch for the scene.',
|
||||
cta: 'Offer Design / Art',
|
||||
to: '/become-artist',
|
||||
to: '/become/artist',
|
||||
color: 'bg-fuchsia-600 hover:bg-fuchsia-700',
|
||||
},
|
||||
graffiti: {
|
||||
|
|
@ -1055,7 +1055,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No graffiti artists yet',
|
||||
body: 'Be the first to offer pieces, murals and skatepark art in your area.',
|
||||
cta: 'Offer Graffiti',
|
||||
to: '/become-graffiti',
|
||||
to: '/become/graffiti',
|
||||
color: 'bg-lime-500 hover:bg-lime-600 text-black',
|
||||
},
|
||||
tattoo: {
|
||||
|
|
@ -1063,7 +1063,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No tattoo artists yet',
|
||||
body: 'Be the first skate-culture tattoo artist to list — flash, custom, fine line and more.',
|
||||
cta: 'Offer Tattoo',
|
||||
to: '/become-tattoo',
|
||||
to: '/become/tattoo',
|
||||
color: 'bg-rose-600 hover:bg-rose-700',
|
||||
},
|
||||
training: {
|
||||
|
|
@ -1071,7 +1071,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No trainers yet',
|
||||
body: 'Be the first trainer or physio to help skaters get strong and stay healthy.',
|
||||
cta: 'Offer Training / Physio',
|
||||
to: '/become-trainer',
|
||||
to: '/become/trainer',
|
||||
color: 'bg-emerald-600 hover:bg-emerald-700',
|
||||
},
|
||||
music: {
|
||||
|
|
@ -1079,7 +1079,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
title: 'No musicians yet',
|
||||
body: 'Skate and make music? Add your band or solo project — genres, instruments and links.',
|
||||
cta: 'Add Music Profile',
|
||||
to: '/become-musician',
|
||||
to: '/become/musician',
|
||||
color: 'bg-sky-500 hover:bg-sky-600',
|
||||
},
|
||||
judging: {
|
||||
|
|
@ -1087,7 +1087,7 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
|
|||
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',
|
||||
to: '/become/judge',
|
||||
color: 'bg-yellow-500 hover:bg-yellow-600 text-black',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -430,7 +430,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
) : null}
|
||||
{isOwnProfile && (
|
||||
<Link
|
||||
to="/become-skater"
|
||||
to="/become/skater"
|
||||
className="text-[10px] font-black uppercase tracking-wider text-[#ff0040] hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Pencil className="h-3 w-3" /> Edit
|
||||
|
|
@ -516,7 +516,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
) : (
|
||||
isOwnProfile && (
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -200,14 +200,14 @@ export default function SkatersPage() {
|
|||
|
||||
<div className="flex flex-wrap items-center gap-3 mt-6">
|
||||
{!myProfile ? (
|
||||
<Link to="/become-skater">
|
||||
<Link to="/become/skater">
|
||||
<Button className="h-11 bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Put me on the map
|
||||
</Button>
|
||||
</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">
|
||||
<Check className="h-4 w-4" />
|
||||
Edit my skater profile
|
||||
|
|
@ -317,7 +317,7 @@ export default function SkatersPage() {
|
|||
<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.
|
||||
</p>
|
||||
<Link to="/become-skater">
|
||||
<Link to="/become/skater">
|
||||
<Button className="bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Put me on the map
|
||||
|
|
|
|||
|
|
@ -720,13 +720,13 @@ export default function SpotDetailPage() {
|
|||
💡 Offer your services at this spot
|
||||
</p>
|
||||
<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">
|
||||
<GraduationCap className="h-4 w-4" />
|
||||
Coach Here
|
||||
</Button>
|
||||
</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">
|
||||
<Video className="h-4 w-4" />
|
||||
Film Here
|
||||
|
|
|
|||
Loading…
Reference in a new issue