- Removed difficulty categorization entirely (type, badges, styles) —
cleaner and no arbitrary skill labels.
- Added many requested/missing tricks with proper research and correct
alt-names, including:
• Rotations category: FS/BS 180, FS/BS 360, half-cab, full cab
(caballerial), fakie 180s.
• Flip tricks: backside flip, frontside flip, FS/BS heelflip, double
heelflip, 360 hardflip, 360 pressure flip.
• Grinds: feeble grind now notes "Grosso grind" aka; added Willy/
Barley, pole jam; inventor credits (Suski, Losi, etc.).
• Fundamentals: nose ollie/north, one-foot, no-foot/nothing, nose
manual, caveman, tic-tac.
• Shoves: nollie shove, ghetto bird.
• Grabs: tailbone, airwalk. Stance/slang: pop, line, kook, sticker
slap, etc. Hardware: durometer.
- Updated the local glossary answerer index to cover all new aka names
(e.g. "grosso grind", "half cab", "north ollie" now answer locally).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
200 lines
8 KiB
TypeScript
200 lines
8 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { useSeoMeta } from '@unhead/react';
|
|
import { Layout } from '@/components/Layout';
|
|
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 (
|
|
<Layout>
|
|
<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 & 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>
|
|
</Layout>
|
|
);
|
|
}
|