SkateSpots/src/pages/GlossaryPage.tsx
SkateSpots 087f490acc refactor(layout): hoist Layout into the router so the chrome mounts once
- AppRouter wraps every route (except /amber-callback and the 404) in ONE
  <Route element={<Layout/>}> with an <Outlet/>; the chrome now persists across
  navigation instead of being torn down and rebuilt on every page.
- Layout: children optional (falls back to <Outlet/>); the map is full-bleed via
  a route check (isFullBleed) instead of a per-page noPadding prop.
- All 59 pages + CraftProfileForm/ErrorState/NewsletterIssueView now render
  fragments instead of <Layout> (structure-preserving), imports dropped.

Removes the whole remount class: chrome no longer re-runs its hooks per
navigation, Sasquatch chat state survives navigation, and the header can't
re-open Amber on every page change.
2026-09-27 13:41:42 +07:00

199 lines
7.9 KiB
TypeScript

import { useState, useMemo } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import {
GLOSSARY,
TOTAL_TRICKS,
searchTricks,
type FlatTrick,
} from '@/lib/glossary';
import { BookOpen, Search, X } from 'lucide-react';
function TrickRow({ trick }: { trick: { name: string; aka?: string[]; desc: string } }) {
return (
<div className="rounded-xl border border-white/8 bg-white/[0.02] p-4 transition-colors hover:border-red-500/25 hover:bg-white/[0.04]">
<h3 className="text-base font-black text-white tracking-tight">{trick.name}</h3>
{trick.aka && trick.aka.length > 0 && (
<p className="mt-1 text-xs text-red-400/80">
<span className="text-gray-500">a.k.a. </span>
{trick.aka.join(' · ')}
</p>
)}
<p className="mt-2 text-sm leading-relaxed text-gray-300">{trick.desc}</p>
</div>
);
}
export default function GlossaryPage() {
const [query, setQuery] = useState('');
const [activeCat, setActiveCat] = useState<string | null>(null);
useSeoMeta({
title: 'Trick Glossary · SkateSpots',
description: `The complete skateboarding glossary — ${TOTAL_TRICKS} entries covering flips, grinds, slides, grabs, airs, lip tricks, handplants, freestyle, transition terms, stance & hardware, with alternate names.`,
});
const isSearching = query.trim().length > 0;
const searchResults: FlatTrick[] = useMemo(
() => (isSearching ? searchTricks(query) : []),
[query, isSearching],
);
const visibleCategories = useMemo(
() => (activeCat ? GLOSSARY.filter((c) => c.id === activeCat) : GLOSSARY),
[activeCat],
);
return (
<>
<div className="min-h-screen">
{/* Hero */}
<div className="relative overflow-hidden border-b border-red-500/20 bg-gradient-to-b from-red-950/20 to-transparent">
<div className="container px-4 py-10 md:py-14">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-red-500/15 text-red-400 shadow-lg shadow-red-500/20">
<BookOpen className="h-6 w-6" />
</div>
<div>
<h1
className="font-display text-3xl md:text-4xl neon-red tracking-wide"
style={{ textShadow: '0 0 10px #ff0040, 0 0 20px #ff0040' }}
>
TRICK GLOSSARY
</h1>
<p className="text-sm text-gray-400">
{TOTAL_TRICKS} entries · tricks, transition, freestyle, stance &amp; hardware
</p>
</div>
</div>
{/* Search */}
<div className="relative mt-6 max-w-xl">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search tricks or nicknames — tre flip, crooks, method…"
className="h-11 rounded-xl border-white/10 bg-black/40 pl-9 pr-9 text-white placeholder:text-gray-500 focus-visible:ring-red-500/40"
/>
{query && (
<button
onClick={() => setQuery('')}
aria-label="Clear search"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white"
>
<X className="h-4 w-4" />
</button>
)}
</div>
</div>
</div>
<div className="container px-4 py-8">
{/* Category filter pills — hidden while searching */}
{!isSearching && (
<div className="mb-8 flex flex-wrap gap-2">
<button
onClick={() => setActiveCat(null)}
className={cn(
'rounded-full border px-3.5 py-1.5 text-xs font-bold uppercase tracking-wide transition-colors',
activeCat === null
? 'border-red-500/50 bg-red-500/15 text-red-400'
: 'border-white/10 text-gray-400 hover:border-white/25 hover:text-white',
)}
>
All
</button>
{GLOSSARY.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveCat(cat.id)}
className={cn(
'rounded-full border px-3.5 py-1.5 text-xs font-bold uppercase tracking-wide transition-colors',
activeCat === cat.id
? 'border-red-500/50 bg-red-500/15 text-red-400'
: 'border-white/10 text-gray-400 hover:border-white/25 hover:text-white',
)}
>
<span className="mr-1">{cat.glyph}</span>
{cat.title}
</button>
))}
</div>
)}
{/* Search results */}
{isSearching ? (
<div>
<p className="mb-4 text-sm text-gray-400">
{searchResults.length} result{searchResults.length === 1 ? '' : 's'} for
<span className="ml-1 font-bold text-white">“{query}”</span>
</p>
{searchResults.length === 0 ? (
<div className="rounded-2xl border border-dashed border-white/10 p-10 text-center">
<p className="text-gray-500">
No tricks match that. Try a nickname like “tre flip”, “crooks”, or “melon”.
</p>
</div>
) : (
<div className="grid gap-3 md:grid-cols-2">
{searchResults.map((t) => (
<div key={`${t.categoryId}-${t.name}`}>
<p className="mb-1 text-[10px] font-bold uppercase tracking-widest text-gray-600">
{t.categoryGlyph} {t.categoryTitle}
</p>
<TrickRow trick={t} />
</div>
))}
</div>
)}
</div>
) : (
/* Category sections */
<div className="space-y-12">
{visibleCategories.map((cat) => (
<section key={cat.id} id={cat.id}>
<div className="mb-4 flex items-baseline gap-3">
<h2 className="text-xl font-black uppercase tracking-wide text-white">
<span className="mr-2">{cat.glyph}</span>
{cat.title}
</h2>
<span className="text-xs text-gray-500">{cat.tricks.length} tricks</span>
</div>
<p className="mb-5 text-sm text-gray-400">{cat.blurb}</p>
<div className="grid gap-3 md:grid-cols-2">
{cat.tricks.map((t) => (
<TrickRow key={t.name} trick={t} />
))}
</div>
</section>
))}
</div>
)}
{/* Footer note */}
<div className="mt-14 rounded-2xl border border-white/8 bg-white/[0.02] p-5 text-center">
<p className="text-sm text-gray-400">
Know a trick or nickname we missed? Tricks pick up different names in every
scene — hit up{' '}
<button
onClick={() =>
window.dispatchEvent(
new CustomEvent('sasquatch:ask', {
detail: { question: 'How do I suggest a trick for the glossary?' },
}),
)
}
className="font-bold text-red-400 hover:text-red-300"
>
The Sasquatch 🦧
</button>{' '}
and let us know.
</p>
</div>
</div>
</div>
</>
);
}