Add the DIY section (resources for boards, ramps & concrete)
New /diy hub: curated resources for making your own gear, with cross-links into the rest of the app. - src/lib/diyGuides.ts: seed data — categories (Boards, Ramps & wooden obstacles, Concrete & DIY spots, Repairs & tuning, Tools & plans), resource cards (Open Source Skateboards / SK8CAD / Instructables / YouTube), and an actions block (find a Builder, find a DIY crew, map a DIY spot, sell builds, find a coach). - src/pages/DiyPage.tsx renders it (category chips + card grid + actions); external links go through sanitizeUrl and credit the source. - Route /diy (lazyWithRetry) + a nav-catalog entry (id diy) so it appears in the desktop "More" menu and the mobile burger; also linked from the site footer and the map links strip. Smoke test walks /diy.
This commit is contained in:
parent
b37d627138
commit
84fde55ddd
10 changed files with 423 additions and 0 deletions
12
AGENTS.md
12
AGENTS.md
|
|
@ -806,6 +806,18 @@ session with **no other memory of this project**. So:
|
|||
burger menu (`MOBILE_MENU_LISTED_HREFS`) now **appends catalog items that
|
||||
aren't hard-coded**, so Marketplace/Mints (and any future section) are always
|
||||
reachable on mobile — don't re-hardcode only a subset.
|
||||
- **DIY resources (`/diy`, 2026-10-05):** a curated hub for making your own
|
||||
boards/ramps/concrete. Content is a seed list in **`src/lib/diyGuides.ts`**
|
||||
(`DIY_CATEGORIES` boards/ramps/concrete/repairs/tools + `DIY_CARDS` +
|
||||
`DIY_ACTIONS`) — vetted external links (Open Source Skateboards / SK8CAD /
|
||||
Instructables / YouTube) and in-app action cards; rendered by
|
||||
`src/pages/DiyPage.tsx`. Registered as a nav-catalog item (id `diy`, Hammer)
|
||||
so it shows in the desktop "More" menu **and** the mobile burger, and linked
|
||||
from the **site footer** + the **map links strip**. Cross-links OUT to
|
||||
`/lab?craft=building` (Builder), `/crews`, `/create-spot`, `/create-listing`,
|
||||
`/lab?craft=coaching`. ⚠️ **Link out, never rehost** — credit the source;
|
||||
`sanitizeUrl` on every external URL. In-app tools (cost calculator) are the
|
||||
next step; community guides would be NIP-23 `30023` with `t=diy`.
|
||||
- **App Shop** (`/app-shop`, `src/pages/AppShopPage.tsx`): SkateSpots merch, to be
|
||||
paid in **sats only (Lightning or Cashu)** — no cards/accounts — sold
|
||||
**locally, on request**: there is **no cart, no checkout, no shipping**. Buying
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ const ROUTES = [
|
|||
'/stats',
|
||||
'/mints',
|
||||
'/app-shop',
|
||||
'/diy',
|
||||
'/review',
|
||||
'/card',
|
||||
'/card/npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const LabPage = lazyWithRetry(() => import("./pages/LabPage"));
|
|||
const WalletPage = lazyWithRetry(() => import("./pages/WalletPage"));
|
||||
const MintsPage = lazyWithRetry(() => import("./pages/MintsPage"));
|
||||
const AppShopPage = lazyWithRetry(() => import("./pages/AppShopPage"));
|
||||
const DiyPage = lazyWithRetry(() => import("./pages/DiyPage"));
|
||||
const AdminReviewPage = lazyWithRetry(() => import("./pages/AdminReviewPage"));
|
||||
const SettingsPage = lazyWithRetry(() => import("./pages/SettingsPage"));
|
||||
const CreateSpotPage = lazyWithRetry(() => import("./pages/CreateSpotPage"));
|
||||
|
|
@ -141,6 +142,7 @@ export function AppRouter() {
|
|||
<Route path="/wallet" element={<WalletPage />} />
|
||||
<Route path="/mints" element={<MintsPage />} />
|
||||
<Route path="/app-shop" element={<AppShopPage />} />
|
||||
<Route path="/diy" element={<DiyPage />} />
|
||||
<Route path="/review" element={<AdminReviewPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/create-spot" element={<CreateSpotPage />} />
|
||||
|
|
|
|||
|
|
@ -1071,6 +1071,12 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<Link
|
||||
to="/diy"
|
||||
className="hover:text-[#ccff00] transition-colors font-medium"
|
||||
>
|
||||
{t('nav.diy')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/newsletter"
|
||||
className="hover:text-[#ccff00] transition-colors font-medium"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export function MapFooterStrip() {
|
|||
if (hidden) return null;
|
||||
|
||||
const links = [
|
||||
{ to: '/diy', label: t('nav.diy') },
|
||||
{ to: '/newsletter', label: t('nav.newsletter') },
|
||||
{ to: '/license', label: t('chromeUi.license') },
|
||||
{ to: '/privacy', label: t('chromeUi.privacy') },
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Coins,
|
||||
Handshake,
|
||||
ShoppingBag,
|
||||
Hammer,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
|
|
@ -62,6 +63,7 @@ export const NAV_CATALOG: NavTab[] = [
|
|||
{ id: 'wallet', href: '/wallet', labelKey: 'nav.wallet', label: 'Wallet', icon: Wallet, authOnly: true },
|
||||
{ id: 'mints', href: '/mints', labelKey: 'nav.mints', label: 'Mints', icon: Coins },
|
||||
{ id: 'app-shop', href: '/app-shop', labelKey: 'nav.appShop', label: 'App Shop', icon: ShoppingBag },
|
||||
{ id: 'diy', href: '/diy', labelKey: 'nav.diy', label: 'DIY', icon: Hammer },
|
||||
{ id: 'trades', href: '/marketplace', labelKey: 'nav.trades', label: 'Marketplace', icon: Handshake },
|
||||
{ id: 'stash', href: '/stash', labelKey: 'nav.stash', label: 'Stash', icon: Package, authOnly: true },
|
||||
{ id: 'collections', href: '/collections', labelKey: 'nav.collections', label: 'Collections', icon: Bookmark, authOnly: true },
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ nav: {
|
|||
marketplace: 'Marketplace',
|
||||
mints: 'Mints',
|
||||
appShop: 'App Shop',
|
||||
diy: 'DIY',
|
||||
trades: 'Marketplace',
|
||||
stash: 'Stash',
|
||||
collections: 'Collections',
|
||||
|
|
@ -789,6 +790,42 @@ nav: {
|
|||
perfectWeather: 'Perfect Weather',
|
||||
},
|
||||
|
||||
diyUi: {
|
||||
title: 'DIY — Make Your Own',
|
||||
tagline: 'Guides, open tools and plans to build your own decks, ramps and concrete spots — then find the people, places and market to go with them.',
|
||||
doItWithPeople: 'Do it with people',
|
||||
source: 'Source',
|
||||
footer: 'Resources link to their original authors — we curate, we don’t rehost. Always work safely: pads, gloves and eye protection.',
|
||||
categories: {
|
||||
boards: 'Boards',
|
||||
ramps: 'Ramps & wooden obstacles',
|
||||
concrete: 'Concrete & DIY spots',
|
||||
repairs: 'Repairs & tuning',
|
||||
tools: 'Tools & plans',
|
||||
},
|
||||
cards: {
|
||||
ossBuild: { title: 'Build a skateboard', blurb: 'Step-by-step deck fabrication — veneer, molds, presses and finishing.' },
|
||||
sk8cad: { title: 'SK8CAD', blurb: 'Open-source CAD for deck shapes, concave and mold design.' },
|
||||
deckCost: { title: 'Deck cost & pricing', blurb: 'Work out materials, labour and a fair selling price before you build.' },
|
||||
ledgeBox: { title: 'Build a ledge or box', blurb: 'A beginner-friendly wooden obstacle you can build in a weekend.' },
|
||||
quarterpipe: { title: 'Build a quarterpipe', blurb: 'Ramp plans, transition and coping for a backyard mini.' },
|
||||
flatbar: { title: 'Build a flatbar', blurb: 'Cheap, portable and the best way to learn rails.' },
|
||||
concreteBasics: { title: 'Concrete basics', blurb: 'Mixing, forming and finishing a ledge or bank that lasts.' },
|
||||
coping: { title: 'Coping & edges', blurb: 'How to set coping and finish a transition for a smooth grind.' },
|
||||
mapSpot: { title: 'Map your DIY spot', blurb: 'Built something? Add it to the map so others can find and skate it.' },
|
||||
regrip: { title: 'Regrip a board', blurb: 'Strip old grip and lay new grip cleanly.' },
|
||||
tune: { title: 'Tune bearings & trucks', blurb: 'Clean bearings, swap bushings and set your truck geometry.' },
|
||||
printMolds: { title: '3D-printed molds', blurb: 'Printable mold and alignment-base files from the community.' },
|
||||
sk8cadTools: { title: 'Mold & alignment tools', blurb: 'Design molds with alignment bases built in.' },
|
||||
},
|
||||
actions: {
|
||||
findBuilder: { title: 'Find a Builder', blurb: 'Ramp and DIY builders in The Lab who can design or pour it for you.' },
|
||||
findCrew: { title: 'Find a DIY crew', blurb: 'Crews focused on building — join the ones turning spots into reality.' },
|
||||
mapSpot: { title: 'Map a DIY spot', blurb: 'Add the spot you built so it lives on the map.' },
|
||||
sellBuilds: { title: 'Sell your builds', blurb: 'List decks, ramps or parts you make in the Marketplace.' },
|
||||
findCoach: { title: 'Find a coach', blurb: 'Coaches who teach DIY skate modules and spot building.' },
|
||||
},
|
||||
},
|
||||
mapUi: {
|
||||
mapFooter: {
|
||||
links: 'Links',
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ nav: {
|
|||
marketplace: 'Mercado',
|
||||
mints: 'Mints',
|
||||
appShop: 'Tienda de la App',
|
||||
diy: 'DIY',
|
||||
trades: 'Mercado',
|
||||
stash: 'Alijo',
|
||||
collections: 'Colecciones',
|
||||
|
|
@ -789,6 +790,42 @@ nav: {
|
|||
perfectWeather: 'Clima Perfecto',
|
||||
},
|
||||
|
||||
diyUi: {
|
||||
title: 'DIY — Hazlo tú mismo',
|
||||
tagline: 'Guías, herramientas abiertas y planos para hacer tus propias tablas, rampas y spots de hormigón — y luego encontrar a la gente, los sitios y el mercado.',
|
||||
doItWithPeople: 'Hazlo con gente',
|
||||
source: 'Fuente',
|
||||
footer: 'Los recursos enlazan a sus autores originales: curamos, no rehospedamos. Trabaja siempre con seguridad: protecciones, guantes y gafas.',
|
||||
categories: {
|
||||
boards: 'Tablas',
|
||||
ramps: 'Rampas y obstáculos de madera',
|
||||
concrete: 'Hormigón y spots DIY',
|
||||
repairs: 'Reparaciones y ajustes',
|
||||
tools: 'Herramientas y planos',
|
||||
},
|
||||
cards: {
|
||||
ossBuild: { title: 'Construye un skate', blurb: 'Fabricación de tablas paso a paso: chapa, moldes, prensas y acabado.' },
|
||||
sk8cad: { title: 'SK8CAD', blurb: 'CAD de código abierto para formas, cóncavo y moldes.' },
|
||||
deckCost: { title: 'Coste y precio de la tabla', blurb: 'Calcula materiales, mano de obra y un precio justo antes de construir.' },
|
||||
ledgeBox: { title: 'Haz un bordillo o cajón', blurb: 'Un obstáculo de madera para principiantes que puedes montar en un fin de semana.' },
|
||||
quarterpipe: { title: 'Haz un quarterpipe', blurb: 'Planos, transición y coping para un mini de patio.' },
|
||||
flatbar: { title: 'Haz un flatbar', blurb: 'Barato, portátil y la mejor forma de aprender a deslizar.' },
|
||||
concreteBasics: { title: 'Fundamentos del hormigón', blurb: 'Mezcla, encofrado y acabado de un bordillo o bank duradero.' },
|
||||
coping: { title: 'Coping y bordes', blurb: 'Cómo colocar el coping y rematar una transición para un grind suave.' },
|
||||
mapSpot: { title: 'Mapea tu spot DIY', blurb: '¿Has construido algo? Añádelo al mapa para que otros lo encuentren.' },
|
||||
regrip: { title: 'Cambia el grip', blurb: 'Quita el grip viejo y coloca el nuevo limpio.' },
|
||||
tune: { title: 'Ajusta rodamientos y ejes', blurb: 'Limpia rodamientos, cambia gomas y ajusta la geometría de los ejes.' },
|
||||
printMolds: { title: 'Moldes impresos en 3D', blurb: 'Archivos de moldes y bases de alineación de la comunidad.' },
|
||||
sk8cadTools: { title: 'Herramientas de moldes', blurb: 'Diseña moldes con bases de alineación integradas.' },
|
||||
},
|
||||
actions: {
|
||||
findBuilder: { title: 'Encuentra un constructor', blurb: 'Constructores de rampas y DIY en The Lab que pueden diseñarlo o hacerlo.' },
|
||||
findCrew: { title: 'Encuentra una crew DIY', blurb: 'Crews centradas en construir — únete a las que levantan spots.' },
|
||||
mapSpot: { title: 'Mapea un spot DIY', blurb: 'Añade el spot que has construido para que viva en el mapa.' },
|
||||
sellBuilds: { title: 'Vende tus creaciones', blurb: 'Publica tablas, rampas o piezas que hagas en el Marketplace.' },
|
||||
findCoach: { title: 'Encuentra un entrenador', blurb: 'Entrenadores que enseñan módulos DIY y construcción de spots.' },
|
||||
},
|
||||
},
|
||||
mapUi: {
|
||||
mapFooter: {
|
||||
links: 'Enlaces',
|
||||
|
|
|
|||
210
src/lib/diyGuides.ts
Normal file
210
src/lib/diyGuides.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Curated DIY resources — how to make your own boards, ramps and concrete spots.
|
||||
*
|
||||
* This is a **seed** list (vetted links + in-app actions), not user content, so
|
||||
* it needs no moderation. Titles/blurbs are i18n keys (`diyUi.*`). To extend,
|
||||
* just add entries here.
|
||||
*
|
||||
* ⚠️ Link out, never rehost: credit the source. External URLs are rendered
|
||||
* through `sanitizeUrl` on the page.
|
||||
*/
|
||||
|
||||
export type DiyCategoryId = 'boards' | 'ramps' | 'concrete' | 'repairs' | 'tools';
|
||||
|
||||
export interface DiyCategory {
|
||||
id: DiyCategoryId;
|
||||
titleKey: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
/** A resource card — either an external guide (`url`) or an in-app action (`to`). */
|
||||
export interface DiyCard {
|
||||
id: string;
|
||||
/** Omit for the cross-link actions block. */
|
||||
category?: DiyCategoryId;
|
||||
titleKey: string;
|
||||
blurbKey: string;
|
||||
emoji: string;
|
||||
/** Shown as a credit for external links. */
|
||||
source?: string;
|
||||
url?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export const DIY_CATEGORIES: DiyCategory[] = [
|
||||
{ id: 'boards', titleKey: 'diyUi.categories.boards', emoji: '🛹' },
|
||||
{ id: 'ramps', titleKey: 'diyUi.categories.ramps', emoji: '🪚' },
|
||||
{ id: 'concrete', titleKey: 'diyUi.categories.concrete', emoji: '🧱' },
|
||||
{ id: 'repairs', titleKey: 'diyUi.categories.repairs', emoji: '🔧' },
|
||||
{ id: 'tools', titleKey: 'diyUi.categories.tools', emoji: '📐' },
|
||||
];
|
||||
|
||||
export const DIY_CARDS: DiyCard[] = [
|
||||
// ── Boards ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'oss-build',
|
||||
category: 'boards',
|
||||
emoji: '🛹',
|
||||
titleKey: 'diyUi.cards.ossBuild.title',
|
||||
blurbKey: 'diyUi.cards.ossBuild.blurb',
|
||||
source: 'Open Source Skateboards',
|
||||
url: 'https://opensourceskateboards.com',
|
||||
},
|
||||
{
|
||||
id: 'sk8cad',
|
||||
category: 'boards',
|
||||
emoji: '🧊',
|
||||
titleKey: 'diyUi.cards.sk8cad.title',
|
||||
blurbKey: 'diyUi.cards.sk8cad.blurb',
|
||||
source: 'Open Source Skateboards',
|
||||
url: 'https://sk8cad.com',
|
||||
},
|
||||
{
|
||||
id: 'deck-cost',
|
||||
category: 'boards',
|
||||
emoji: '💰',
|
||||
titleKey: 'diyUi.cards.deckCost.title',
|
||||
blurbKey: 'diyUi.cards.deckCost.blurb',
|
||||
source: 'Open Source Skateboards',
|
||||
url: 'https://opensourceskateboards.com',
|
||||
},
|
||||
|
||||
// ── Ramps & wooden obstacles ─────────────────────────────────────────────
|
||||
{
|
||||
id: 'ledge-box',
|
||||
category: 'ramps',
|
||||
emoji: '📦',
|
||||
titleKey: 'diyUi.cards.ledgeBox.title',
|
||||
blurbKey: 'diyUi.cards.ledgeBox.blurb',
|
||||
source: 'Instructables',
|
||||
url: 'https://www.instructables.com/howto/skate+ledge/',
|
||||
},
|
||||
{
|
||||
id: 'quarterpipe',
|
||||
category: 'ramps',
|
||||
emoji: '🌊',
|
||||
titleKey: 'diyUi.cards.quarterpipe.title',
|
||||
blurbKey: 'diyUi.cards.quarterpipe.blurb',
|
||||
source: 'Instructables',
|
||||
url: 'https://www.instructables.com/howto/skate+ramp/',
|
||||
},
|
||||
{
|
||||
id: 'flatbar',
|
||||
category: 'ramps',
|
||||
emoji: '➖',
|
||||
titleKey: 'diyUi.cards.flatbar.title',
|
||||
blurbKey: 'diyUi.cards.flatbar.blurb',
|
||||
source: 'YouTube',
|
||||
url: 'https://www.youtube.com/results?search_query=how+to+build+a+skate+flatbar',
|
||||
},
|
||||
|
||||
// ── Concrete & DIY spots ─────────────────────────────────────────────────
|
||||
{
|
||||
id: 'concrete-basics',
|
||||
category: 'concrete',
|
||||
emoji: '🧱',
|
||||
titleKey: 'diyUi.cards.concreteBasics.title',
|
||||
blurbKey: 'diyUi.cards.concreteBasics.blurb',
|
||||
source: 'Instructables',
|
||||
url: 'https://www.instructables.com/howto/concrete+skatepark/',
|
||||
},
|
||||
{
|
||||
id: 'coping',
|
||||
category: 'concrete',
|
||||
emoji: '🛞',
|
||||
titleKey: 'diyUi.cards.coping.title',
|
||||
blurbKey: 'diyUi.cards.coping.blurb',
|
||||
source: 'YouTube',
|
||||
url: 'https://www.youtube.com/results?search_query=diy+skatepark+coping',
|
||||
},
|
||||
{
|
||||
id: 'map-spot',
|
||||
category: 'concrete',
|
||||
emoji: '📍',
|
||||
titleKey: 'diyUi.cards.mapSpot.title',
|
||||
blurbKey: 'diyUi.cards.mapSpot.blurb',
|
||||
to: '/create-spot',
|
||||
},
|
||||
|
||||
// ── Repairs & tuning ─────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'regrip',
|
||||
category: 'repairs',
|
||||
emoji: '🩹',
|
||||
titleKey: 'diyUi.cards.regrip.title',
|
||||
blurbKey: 'diyUi.cards.regrip.blurb',
|
||||
source: 'YouTube',
|
||||
url: 'https://www.youtube.com/results?search_query=how+to+regrip+a+skateboard',
|
||||
},
|
||||
{
|
||||
id: 'tune',
|
||||
category: 'repairs',
|
||||
emoji: '🔩',
|
||||
titleKey: 'diyUi.cards.tune.title',
|
||||
blurbKey: 'diyUi.cards.tune.blurb',
|
||||
source: 'YouTube',
|
||||
url: 'https://www.youtube.com/results?search_query=clean+skateboard+bearings+bushings',
|
||||
},
|
||||
|
||||
// ── Tools & plans ────────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'print-molds',
|
||||
category: 'tools',
|
||||
emoji: '🖨️',
|
||||
titleKey: 'diyUi.cards.printMolds.title',
|
||||
blurbKey: 'diyUi.cards.printMolds.blurb',
|
||||
source: 'Printables',
|
||||
url: 'https://www.printables.com/search/models?q=skateboard+mold',
|
||||
},
|
||||
{
|
||||
id: 'sk8cad-tools',
|
||||
category: 'tools',
|
||||
emoji: '📐',
|
||||
titleKey: 'diyUi.cards.sk8cadTools.title',
|
||||
blurbKey: 'diyUi.cards.sk8cadTools.blurb',
|
||||
source: 'Open Source Skateboards',
|
||||
url: 'https://sk8cad.com',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Cross-links into the rest of the app — the point of the section: turn a guide
|
||||
* into a person, a place, or a sale.
|
||||
*/
|
||||
export const DIY_ACTIONS: DiyCard[] = [
|
||||
{
|
||||
id: 'find-builder',
|
||||
emoji: '🧱',
|
||||
titleKey: 'diyUi.actions.findBuilder.title',
|
||||
blurbKey: 'diyUi.actions.findBuilder.blurb',
|
||||
to: '/lab?craft=building',
|
||||
},
|
||||
{
|
||||
id: 'find-crew',
|
||||
emoji: '👥',
|
||||
titleKey: 'diyUi.actions.findCrew.title',
|
||||
blurbKey: 'diyUi.actions.findCrew.blurb',
|
||||
to: '/crews',
|
||||
},
|
||||
{
|
||||
id: 'map-spot-action',
|
||||
emoji: '📍',
|
||||
titleKey: 'diyUi.actions.mapSpot.title',
|
||||
blurbKey: 'diyUi.actions.mapSpot.blurb',
|
||||
to: '/create-spot',
|
||||
},
|
||||
{
|
||||
id: 'sell-builds',
|
||||
emoji: '🛒',
|
||||
titleKey: 'diyUi.actions.sellBuilds.title',
|
||||
blurbKey: 'diyUi.actions.sellBuilds.blurb',
|
||||
to: '/create-listing',
|
||||
},
|
||||
{
|
||||
id: 'find-coach',
|
||||
emoji: '🎓',
|
||||
titleKey: 'diyUi.actions.findCoach.title',
|
||||
blurbKey: 'diyUi.actions.findCoach.blurb',
|
||||
to: '/lab?craft=coaching',
|
||||
},
|
||||
];
|
||||
115
src/pages/DiyPage.tsx
Normal file
115
src/pages/DiyPage.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Hammer, ExternalLink, ChevronRight } from 'lucide-react';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DIY_CATEGORIES, DIY_CARDS, DIY_ACTIONS, type DiyCard } from '@/lib/diyGuides';
|
||||
|
||||
/**
|
||||
* DIY — curated resources for making your own boards, ramps and concrete spots,
|
||||
* with cross-links into the rest of the app (find a builder, map a spot, sell).
|
||||
* Content lives in `src/lib/diyGuides.ts`.
|
||||
*/
|
||||
function DiyCardView({ card }: { card: DiyCard }) {
|
||||
const { t } = useTranslation();
|
||||
const external = !!card.url;
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<span className="text-2xl leading-none" aria-hidden>{card.emoji}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-bold text-white">{t(card.titleKey)}</span>
|
||||
<span className="mt-0.5 block text-xs leading-relaxed text-gray-400">{t(card.blurbKey)}</span>
|
||||
{card.source && (
|
||||
<span className="mt-1 block text-[10px] text-gray-500">{t('diyUi.source')}: {card.source}</span>
|
||||
)}
|
||||
</span>
|
||||
{external
|
||||
? <ExternalLink className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-500" />
|
||||
: <ChevronRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-500" />}
|
||||
</>
|
||||
);
|
||||
|
||||
const cls = cn(
|
||||
'group flex items-start gap-3 rounded-2xl border border-white/10 bg-white/[0.03] p-4 transition-colors',
|
||||
'hover:border-[#ccff00]/40 hover:bg-white/[0.06]',
|
||||
);
|
||||
|
||||
if (external) {
|
||||
return (
|
||||
<a href={sanitizeUrl(card.url)!} target="_blank" rel="noopener noreferrer" className={cls}>
|
||||
{body}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <Link to={card.to!} className={cls}>{body}</Link>;
|
||||
}
|
||||
|
||||
export default function DiyPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useSeoMeta({
|
||||
title: 'DIY — Make Your Own | SkateSpots',
|
||||
description: 'Guides, open tools and plans to make your own skateboards, ramps and concrete spots — and the people and places to do it with.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-5xl px-4 py-8">
|
||||
{/* Header */}
|
||||
<header className="mb-6">
|
||||
<h1 className="flex items-center gap-2 text-3xl md:text-4xl font-black uppercase tracking-tight text-white">
|
||||
<Hammer className="h-7 w-7 text-[#ccff00]" />
|
||||
{t('diyUi.title')}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-400 max-w-2xl">{t('diyUi.tagline')}</p>
|
||||
</header>
|
||||
|
||||
{/* Category chips */}
|
||||
<nav className="sticky top-14 md:top-16 z-20 -mx-4 mb-6 flex gap-2 overflow-x-auto border-b border-white/5 bg-black/90 px-4 py-2.5 backdrop-blur scrollbar-hide">
|
||||
{DIY_CATEGORIES.map((c) => (
|
||||
<a
|
||||
key={c.id}
|
||||
href={`#${c.id}`}
|
||||
className="flex shrink-0 min-h-0 items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-bold text-gray-300 transition-colors hover:border-[#ccff00]/40 hover:text-white"
|
||||
>
|
||||
<span aria-hidden>{c.emoji}</span>
|
||||
{t(c.titleKey)}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Resource sections */}
|
||||
{DIY_CATEGORIES.map((cat) => {
|
||||
const cards = DIY_CARDS.filter((c) => c.category === cat.id);
|
||||
if (cards.length === 0) return null;
|
||||
return (
|
||||
<section key={cat.id} id={cat.id} className="mb-10 scroll-mt-28">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-lg font-black uppercase tracking-wide text-[#ccff00]">
|
||||
<span aria-hidden>{cat.emoji}</span>
|
||||
{t(cat.titleKey)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{cards.map((card) => <DiyCardView key={card.id} card={card} />)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Do it with people — cross-links into the app */}
|
||||
<section className="mb-10">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-lg font-black uppercase tracking-wide text-[#ccff00]">
|
||||
<span aria-hidden>🤝</span>
|
||||
{t('diyUi.doItWithPeople')}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{DIY_ACTIONS.map((card) => <DiyCardView key={card.id} card={card} />)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="mt-6 text-center text-[11px] leading-relaxed text-gray-600">
|
||||
{t('diyUi.footer')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue