diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index 39e82f2..13887cb 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -56,6 +56,7 @@ const RiderCardPage = lazy(() => import("./pages/RiderCardPage"));
const AmberCallbackPage = lazy(() => import("./pages/AmberCallbackPage"));
const MarketplacePage = lazy(() => import("./pages/MarketplacePage"));
const NewsletterPage = lazy(() => import("./pages/NewsletterPage"));
+const GlossaryPage = lazy(() => import("./pages/GlossaryPage"));
const NotFound = lazy(() => import("./pages/NotFound"));
// Minimal inline fallback — no extra component needed
@@ -136,6 +137,8 @@ export function AppRouter() {
} />
{/* Newsletter */}
} />
+ {/* Trick Glossary */}
+ } />
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index a5af17c..08d1bb9 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -49,6 +49,7 @@ import {
Radio,
Users,
SlidersHorizontal,
+ BookOpen,
} from 'lucide-react';
import { OfflineIndicator } from '@/components/OfflineIndicator';
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
@@ -474,6 +475,12 @@ export function Layout({ children, noPadding }: LayoutProps) {
))}
+
+
+
+ Glossary
+
+
@@ -713,6 +720,13 @@ export function Layout({ children, noPadding }: LayoutProps) {
isActive={isActive('/stats')}
onClick={() => setMobileMenuOpen(false)}
/>
+ setMobileMenuOpen(false)}
+ />
{
});
}
-// ─── OpenRouter free model — no credits, no NIP-98, works for everyone ────────
-const OPENROUTER_API_KEY = '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';
-const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
-const FREE_MODEL = 'meta-llama/llama-3.3-70b-instruct:free';
-
-// Try multiple free models in order — if one is rate-limited, fall back to the next
-const FREE_MODELS = [
- 'openai/gpt-oss-20b:free', // Fast, reliable
- 'google/gemma-4-31b-it:free', // Good fallback
- 'meta-llama/llama-3.3-70b-instruct:free', // Best quality, sometimes rate-limited
-];
-
-async function callAI(messages: Array<{ role: 'user' | 'assistant'; content: string }>): Promise {
- let lastError = '';
- for (const model of FREE_MODELS) {
- try {
- const response = await fetch(OPENROUTER_URL, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': 'https://skate-spots.org',
- 'X-Title': 'SkateSpots',
- },
- body: JSON.stringify({
- model,
- messages,
- max_tokens: 400,
- }),
- });
- if (!response.ok) {
- const err = await response.text();
- console.warn(`[Sasquatch] model ${model} failed ${response.status}:`, err);
- lastError = `${response.status}: ${err}`;
- continue; // try next model
- }
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content ?? '';
- if (content) return content;
- } catch (err) {
- console.warn(`[Sasquatch] model ${model} threw:`, err);
- lastError = String(err);
- }
- }
- throw new Error(`All AI models failed. Last error: ${lastError}`);
-}
+// AI calls route through the shared Sasquatch brain (sasquatchBrain.ts), which
+// handles the free-model fallback list + rate-limit backoff/retry in one place.
+const callAI = callSasquatchAI;
export type SasquatchContext = 'home' | 'onboarding' | 'map' | 'lab';
@@ -597,7 +555,7 @@ Instructions:
if (!aiSucceeded) {
const fallback = staticAnswer ??
- "The AI is a bit slow right now (free tier rate limits). Try again in a moment, or check the 🗺️ Map for spots, 🧪 The Lab for coaches. 🛹";
+ "My brain's catching a lot of traffic right now 🦧 — give it a few seconds and ask again. Meanwhile you can hit the 🗺️ Map for spots, 🧪 The Lab for coaches, or the 📖 Glossary to level up your trick vocab. 🛹";
appendMessage({ role: 'sasquatch', text: fallback, timestamp: Date.now() });
}
} catch (err) {
diff --git a/src/lib/glossary.ts b/src/lib/glossary.ts
new file mode 100644
index 0000000..d0fe2e5
--- /dev/null
+++ b/src/lib/glossary.ts
@@ -0,0 +1,239 @@
+/**
+ * Skateboarding trick glossary.
+ * ─────────────────────────────────────────────────────────────────────────────
+ * A curated, categorized reference of skate tricks with descriptions and their
+ * common alternate names (tricks pick up regional / era-specific nicknames).
+ *
+ * Static content — no network, no Nostr. Purely a learning reference for the
+ * community. Keep entries concise and skater-accurate.
+ */
+
+export interface Trick {
+ /** Canonical name. */
+ name: string;
+ /** Other names the same trick is known by. */
+ aka?: string[];
+ /** Short, skater-accurate description. */
+ desc: string;
+ /** Difficulty hint for filtering. */
+ level?: 'beginner' | 'intermediate' | 'advanced';
+}
+
+export interface TrickCategory {
+ id: string;
+ /** Display title. */
+ title: string;
+ /** One-line description of the category. */
+ blurb: string;
+ /** Emoji used as a lightweight category glyph. */
+ glyph: string;
+ tricks: Trick[];
+}
+
+export const GLOSSARY: TrickCategory[] = [
+ {
+ id: 'fundamentals',
+ title: 'Fundamentals',
+ blurb: 'The building blocks every trick is stacked on.',
+ glyph: '🛹',
+ tricks: [
+ { name: 'Ollie', desc: 'The foundational jump — pop the tail, slide the front foot, and lift the board with no hands.', level: 'beginner' },
+ { name: 'Nollie', aka: ['Nose Ollie'], desc: 'An ollie popped off the nose instead of the tail, riding forward.', level: 'intermediate' },
+ { name: 'Fakie Ollie', desc: 'An ollie performed while rolling backwards (fakie stance).', level: 'beginner' },
+ { name: 'Switch Ollie', desc: 'An ollie done in your non-natural (switch) stance.', level: 'intermediate' },
+ { name: 'Pop Shove-it', aka: ['Pop Shuv', 'Shove-it'], desc: 'Spin the board 180° under your feet without flipping it, with pop.', level: 'beginner' },
+ { name: 'Manual', aka: ['Wheelie'], desc: 'Balance rolling on the back two wheels; nose manual uses the front two.', level: 'beginner' },
+ { name: 'Powerslide', desc: 'Turn the board perpendicular and slide the wheels to scrub speed.', level: 'beginner' },
+ { name: 'Boardslide', desc: 'Slide along an obstacle with the board perpendicular, deck contacting the edge.', level: 'intermediate' },
+ ],
+ },
+ {
+ id: 'flips',
+ title: 'Flip Tricks',
+ blurb: 'Tricks where the board rotates around one or more axes.',
+ glyph: '🌀',
+ tricks: [
+ { name: 'Kickflip', aka: ['Magic Flip (original name)'], desc: 'Flick the board so it flips one full rotation along its length toward the heel side of the toes.', level: 'intermediate' },
+ { name: 'Heelflip', desc: 'Like a kickflip but flicked off the heel, flipping the opposite direction.', level: 'intermediate' },
+ { name: 'Varial Kickflip', desc: 'A kickflip combined with a backside pop shove-it.', level: 'intermediate' },
+ { name: 'Varial Heelflip', desc: 'A heelflip combined with a frontside pop shove-it.', level: 'intermediate' },
+ { name: '360 Flip', aka: ['Tre Flip', '360 Kickflip', 'Three-Sixty Flip'], desc: 'A kickflip combined with a 360° backside pop shove-it — a signature street trick.', level: 'advanced' },
+ { name: 'Hardflip', desc: 'A frontside pop shove-it combined with a kickflip; the board rotates vertically through the legs.', level: 'advanced' },
+ { name: 'Inward Heelflip', desc: 'A backside pop shove-it combined with a heelflip.', level: 'advanced' },
+ { name: 'Laser Flip', desc: 'A frontside 360 pop shove-it combined with a heelflip — the mirror of a tre flip.', level: 'advanced' },
+ { name: 'Double Kickflip', desc: 'A kickflip that rotates twice before you catch it.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'shoves',
+ title: 'Shove-its & Spins',
+ blurb: 'Board and body rotations without a flip.',
+ glyph: '🔄',
+ tricks: [
+ { name: 'Shove-it', aka: ['Shuvit'], desc: 'Scoop the board 180° flat under your feet without popping.', level: 'beginner' },
+ { name: 'Frontside Shove-it', desc: 'A shove-it where the board spins frontside (toward your toes).', level: 'beginner' },
+ { name: '360 Shove-it', aka: ['360 Shuv'], desc: 'A shove-it that spins the board a full 360°.', level: 'intermediate' },
+ { name: 'Frontside 180', aka: ['FS 180'], desc: 'An ollie with a 180° body-and-board rotation frontside.', level: 'intermediate' },
+ { name: 'Backside 180', aka: ['BS 180'], desc: 'An ollie with a 180° body-and-board rotation backside.', level: 'intermediate' },
+ { name: 'Bigspin', desc: 'A 360° board shove-it combined with a 180° body rotation, same direction.', level: 'advanced' },
+ { name: 'Bigflip', desc: 'A bigspin with a kickflip added.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'grinds',
+ title: 'Grinds',
+ blurb: 'Sliding along an edge on the trucks.',
+ glyph: '⚙️',
+ tricks: [
+ { name: '50-50', aka: ['Fifty-Fifty'], desc: 'Grind on both trucks equally along a rail or ledge.', level: 'intermediate' },
+ { name: '5-0', aka: ['Five-O'], desc: 'Grind on the back truck only, nose up (like a manual on an edge).', level: 'intermediate' },
+ { name: 'Nosegrind', desc: 'Grind on the front truck only, tail up.', level: 'intermediate' },
+ { name: 'Crooked Grind', aka: ['K-Grind', 'Crooks'], desc: 'A nosegrind angled so the nose also slides on the edge.', level: 'advanced' },
+ { name: 'Smith Grind', desc: 'Back truck grinds while the front dips below the edge, nose pointed down.', level: 'advanced' },
+ { name: 'Feeble Grind', desc: 'Back truck grinds with the front truck hanging over the far side, nose down.', level: 'advanced' },
+ { name: 'Overcrook', aka: ['Overcrooked'], desc: 'A crooked grind on the far side of the rail with the nose overhanging.', level: 'advanced' },
+ { name: 'Salad Grind', desc: 'A 5-0 variation with the back truck grinding and rear wheels on the far side.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'slides',
+ title: 'Slides',
+ blurb: 'Sliding on the deck instead of the trucks.',
+ glyph: '➖',
+ tricks: [
+ { name: 'Boardslide', desc: 'Slide with the middle of the deck perpendicular across the edge.', level: 'intermediate' },
+ { name: 'Lipslide', desc: 'Like a boardslide but the back of the board clears the obstacle first.', level: 'advanced' },
+ { name: 'Noseslide', desc: 'Slide on the nose of the board along a ledge.', level: 'intermediate' },
+ { name: 'Tailslide', desc: 'Slide on the tail of the board along a ledge.', level: 'advanced' },
+ { name: 'Bluntslide', desc: 'Slide with the back wheels up on the ledge and tail on the surface.', level: 'advanced' },
+ { name: 'Darkslide', desc: 'Slide upside-down on the grip tape — an advanced flip-in slide.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'grabs',
+ title: 'Grabs',
+ blurb: 'Airborne tricks where a hand grabs the board.',
+ glyph: '✋',
+ tricks: [
+ { name: 'Indy', aka: ['Indy Grab'], desc: 'Trailing hand grabs the toe edge between the feet.', level: 'intermediate' },
+ { name: 'Melon', aka: ['Melancholie'], desc: 'Leading hand grabs the heel edge between the feet.', level: 'intermediate' },
+ { name: 'Mute', aka: ['Mute Grab'], desc: 'Leading hand grabs the toe edge; named after deaf skater Chris Weddle.', level: 'intermediate' },
+ { name: 'Nosegrab', desc: 'Grab the nose of the board in the air.', level: 'intermediate' },
+ { name: 'Tailgrab', desc: 'Grab the tail of the board in the air.', level: 'intermediate' },
+ { name: 'Stalefish', desc: 'Trailing hand grabs the heel edge behind the back leg.', level: 'advanced' },
+ { name: 'Method', aka: ['Method Air'], desc: 'Heel-edge grab with the board pulled up behind you — a stylish, arched air.', level: 'advanced' },
+ { name: 'Nosebone', desc: 'A nose grab with the front leg boned out straight.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'airs',
+ title: 'Airs (Vert / Transition)',
+ blurb: 'Big air tricks off ramps, bowls, and quarterpipes.',
+ glyph: '🚀',
+ tricks: [
+ { name: 'Air', aka: ['Aerial'], desc: 'Any launch off a transition with the board leaving the coping.', level: 'intermediate' },
+ { name: 'Backside Air', desc: 'An air rotating backside out of the transition, usually grabbed.', level: 'advanced' },
+ { name: 'Frontside Air', desc: 'An air rotating frontside out of the transition.', level: 'advanced' },
+ { name: '540', aka: ['Five-Forty', 'McTwist (with flip)'], desc: 'A one-and-a-half rotation air; the McTwist adds a frontside flip.', level: 'advanced' },
+ { name: '900', desc: 'Two-and-a-half rotations — famously landed by Tony Hawk in 1999.', level: 'advanced' },
+ { name: 'Judo Air', desc: 'A grab air with the front foot kicked off and out to the side.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'lip',
+ title: 'Lip Tricks',
+ blurb: 'Tricks performed on the coping / lip of a ramp or bowl.',
+ glyph: '👄',
+ tricks: [
+ { name: 'Rock to Fakie', aka: ['Rock n Roll to Fakie'], desc: 'Ride up, rock the board over the coping, then roll back in fakie.', level: 'intermediate' },
+ { name: 'Rock n Roll', desc: 'Rock over the coping then pivot 180° to roll back in forward.', level: 'intermediate' },
+ { name: 'Axle Stall', aka: ['50-50 Stall'], desc: 'Stall on both trucks on the coping, then drop back in.', level: 'intermediate' },
+ { name: 'Feeble Stall', desc: 'A feeble grind held as a stall on the coping.', level: 'advanced' },
+ { name: 'Blunt to Fakie', desc: 'Stall in a blunt position on the coping then come back in fakie.', level: 'advanced' },
+ { name: 'Disaster', desc: 'Ollie over the coping so the middle of the board rests on the lip.', level: 'advanced' },
+ { name: 'Smith Stall', desc: 'A smith grind held as a stall on the coping before dropping back in.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'stalls',
+ title: 'Stalls',
+ blurb: 'Pausing balanced on an obstacle before re-entering.',
+ glyph: '⏸️',
+ tricks: [
+ { name: 'Nose Stall', desc: 'Balance stalled on the nose against a ledge or coping.', level: 'intermediate' },
+ { name: 'Tail Stall', desc: 'Balance stalled on the tail on the coping.', level: 'intermediate' },
+ { name: 'Pivot Stall', desc: 'Stall balanced on the back truck and pivot to re-enter.', level: 'advanced' },
+ { name: 'Nose Pickup', aka: ['Nose Stall Pickup'], desc: 'Stall on the nose then pick the board back up to roll away.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'handplants',
+ title: 'Handplants & Inverts',
+ blurb: 'One-hand-on-the-coping inverted tricks — old-school flair.',
+ glyph: '🤸',
+ tricks: [
+ { name: 'Invert', aka: ['Handplant', 'One-Hand Plant'], desc: 'Plant one hand on the coping and balance inverted with the board grabbed.', level: 'advanced' },
+ { name: 'Andrecht', desc: 'A backside invert grabbing heel-side with the plant hand behind.', level: 'advanced' },
+ { name: 'Eggplant', desc: 'A frontside invert planting the leading hand.', level: 'advanced' },
+ { name: 'Sad Plant', desc: 'A handplant variation with a straightened, "sad" board position.', level: 'advanced' },
+ { name: 'Ho-Ho', aka: ['Two-Hand Plant'], desc: 'A handstand-style invert balanced on both hands.', level: 'advanced' },
+ ],
+ },
+ {
+ id: 'boneless',
+ title: 'Boneless & Foot-Plants',
+ blurb: 'Grab-and-jump tricks planting a foot on the ground.',
+ glyph: '🦴',
+ tricks: [
+ { name: 'Boneless', desc: 'Grab the board, plant the front foot on the ground, and jump back on.', level: 'intermediate' },
+ { name: 'No Comply', desc: 'Step the front foot off, pop the tail so the board spins/hops, then reload.', level: 'intermediate' },
+ { name: 'Bean Plant', desc: 'A boneless variation planting the back foot and grabbing with the leading hand.', level: 'intermediate' },
+ { name: 'Nose Bonk', desc: 'Tap the nose off an obstacle mid-line for style.', level: 'beginner' },
+ ],
+ },
+ {
+ id: 'freestyle',
+ title: 'Freestyle',
+ blurb: 'Flatground balance and pivot tricks from skate’s roots.',
+ glyph: '🎪',
+ tricks: [
+ { name: 'Casper', aka: ['Casper Slide'], desc: 'Flip the board upside-down and balance on the tail with one foot under the deck.', level: 'advanced' },
+ { name: 'Primo', aka: ['Primo Slide'], desc: 'Balance the board on its side (rail) and stand on it.', level: 'advanced' },
+ { name: 'Rail Stand', aka: ['Primo Stand'], desc: 'Stand on the upturned rail of the board — classic freestyle balance.', level: 'advanced' },
+ { name: 'Truckstand', desc: 'Balance on one truck with the board tilted vertically.', level: 'advanced' },
+ { name: 'Pogo', desc: 'Hop on the tail with the board vertical, holding the nose.', level: 'advanced' },
+ { name: 'Handstand', aka: ['Handstand Fingerflip'], desc: 'A handstand on the board, often flipped out — a freestyle staple.', level: 'advanced' },
+ ],
+ },
+];
+
+/** Total trick count across all categories (for headers/stats). */
+export const TOTAL_TRICKS = GLOSSARY.reduce((sum, c) => sum + c.tricks.length, 0);
+
+/** Flat, searchable list joining name + aka + description + category. */
+export interface FlatTrick extends Trick {
+ categoryId: string;
+ categoryTitle: string;
+ categoryGlyph: string;
+}
+
+export const ALL_TRICKS: FlatTrick[] = GLOSSARY.flatMap((cat) =>
+ cat.tricks.map((t) => ({
+ ...t,
+ categoryId: cat.id,
+ categoryTitle: cat.title,
+ categoryGlyph: cat.glyph,
+ })),
+);
+
+/** Case-insensitive search across name, aka, and description. */
+export function searchTricks(query: string): FlatTrick[] {
+ const q = query.trim().toLowerCase();
+ if (!q) return ALL_TRICKS;
+ return ALL_TRICKS.filter((t) => {
+ if (t.name.toLowerCase().includes(q)) return true;
+ if (t.desc.toLowerCase().includes(q)) return true;
+ if (t.aka?.some((a) => a.toLowerCase().includes(q))) return true;
+ return false;
+ });
+}
diff --git a/src/lib/sasquatchBrain.ts b/src/lib/sasquatchBrain.ts
index ba69540..5275c63 100644
--- a/src/lib/sasquatchBrain.ts
+++ b/src/lib/sasquatchBrain.ts
@@ -9,43 +9,77 @@ const OPENROUTER_API_KEY = 'sk-or-v1-ce29b4c58cd842212e4b2f1fae7eb31ab85ce93870b
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
// Try multiple free models in order — if one is rate-limited, fall back.
+// Slugs verified against OpenRouter's free tier. Ordered by reliability +
+// availability so the first pick is usually the one that answers. If any is
+// rate-limited (429) we back off briefly then move to the next model.
export const FREE_MODELS = [
- 'openai/gpt-oss-20b:free',
- 'google/gemma-4-31b-it:free',
'meta-llama/llama-3.3-70b-instruct:free',
+ 'google/gemma-2-9b-it:free',
+ 'mistralai/mistral-7b-instruct:free',
+ 'qwen/qwen-2.5-7b-instruct:free',
+ 'openai/gpt-oss-20b:free',
];
export type ChatTurn = { role: 'user' | 'assistant'; content: string };
-/** Call the AI with model fallback. Throws only if every model fails. */
+const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+/**
+ * Call the AI with model fallback + light retry on rate limits.
+ *
+ * Strategy: for each model, try once; on a 429 (rate limit) wait a short,
+ * increasing beat and retry that same model once before moving on. This
+ * smooths over the free tier's bursty limits without hammering the API.
+ * Throws only if every model fails after its retries.
+ */
export async function callSasquatchAI(messages: ChatTurn[], maxTokens = 400): Promise {
let lastError = '';
+
for (const model of FREE_MODELS) {
- try {
- const response = await fetch(OPENROUTER_URL, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
- 'Content-Type': 'application/json',
- 'HTTP-Referer': 'https://skate-spots.org',
- 'X-Title': 'SkateSpots',
- },
- body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
- });
- if (!response.ok) {
- const err = await response.text();
- console.warn(`[Sasquatch] model ${model} failed ${response.status}:`, err);
- lastError = `${response.status}: ${err}`;
- continue;
+ // Up to 2 attempts per model (initial + 1 retry after backoff on 429).
+ for (let attempt = 0; attempt < 2; attempt++) {
+ try {
+ const response = await fetch(OPENROUTER_URL, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
+ 'Content-Type': 'application/json',
+ 'HTTP-Referer': 'https://skate-spots.org',
+ 'X-Title': 'SkateSpots',
+ },
+ body: JSON.stringify({ model, messages, max_tokens: maxTokens }),
+ });
+
+ if (response.status === 429 && attempt === 0) {
+ // Rate limited — respect Retry-After if present, else short backoff.
+ const retryAfter = Number(response.headers.get('retry-after'));
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
+ ? Math.min(retryAfter * 1000, 2500)
+ : 600;
+ console.warn(`[Sasquatch] ${model} rate-limited, retrying in ${waitMs}ms`);
+ await sleep(waitMs);
+ continue; // retry same model
+ }
+
+ if (!response.ok) {
+ const err = await response.text();
+ console.warn(`[Sasquatch] model ${model} failed ${response.status}:`, err);
+ lastError = `${response.status}: ${err}`;
+ break; // move to next model
+ }
+
+ const data = await response.json();
+ const content = data.choices?.[0]?.message?.content ?? '';
+ if (content) return content;
+ break; // empty content — try next model
+ } catch (err) {
+ console.warn(`[Sasquatch] model ${model} threw:`, err);
+ lastError = String(err);
+ break; // network error — try next model
}
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content ?? '';
- if (content) return content;
- } catch (err) {
- console.warn(`[Sasquatch] model ${model} threw:`, err);
- lastError = String(err);
}
}
+
throw new Error(`All AI models failed. Last error: ${lastError}`);
}
diff --git a/src/pages/GlossaryPage.tsx b/src/pages/GlossaryPage.tsx
new file mode 100644
index 0000000..cfc9760
--- /dev/null
+++ b/src/pages/GlossaryPage.tsx
@@ -0,0 +1,217 @@
+import { useState, useMemo } from 'react';
+import { useSeoMeta } from '@unhead/react';
+import { Layout } from '@/components/Layout';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { cn } from '@/lib/utils';
+import {
+ GLOSSARY,
+ TOTAL_TRICKS,
+ searchTricks,
+ type FlatTrick,
+} from '@/lib/glossary';
+import { BookOpen, Search, X } from 'lucide-react';
+
+const LEVEL_STYLES: Record = {
+ beginner: 'bg-green-500/15 text-green-400 border-green-500/30',
+ intermediate: 'bg-amber-500/15 text-amber-400 border-amber-500/30',
+ advanced: 'bg-red-500/15 text-red-400 border-red-500/30',
+};
+
+function TrickRow({ trick }: { trick: { name: string; aka?: string[]; desc: string; level?: string } }) {
+ return (
+