200 lines
7.9 KiB
TypeScript
200 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';
|
|
import { useTranslation } from '@/contexts/language';
|
|
|
|
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 { t } = useTranslation();
|
|
const [query, setQuery] = useState('');
|
|
const [activeCat, setActiveCat] = useState<string | null>(null);
|
|
|
|
useSeoMeta({
|
|
title: t('settingsUi.glossary.seoTitle'),
|
|
description: t('settingsUi.glossary.seoDescription', { count: TOTAL_TRICKS }),
|
|
});
|
|
|
|
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' }}
|
|
>
|
|
{t('settingsUi.glossary.heading')}
|
|
</h1>
|
|
<p className="text-sm text-gray-400">
|
|
{t('settingsUi.glossary.subheading', { count: TOTAL_TRICKS })}
|
|
</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={t('settingsUi.glossary.searchPlaceholder')}
|
|
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={t('settingsUi.glossary.clearSearch')}
|
|
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',
|
|
)}
|
|
>
|
|
{t('settingsUi.glossary.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">
|
|
{t('settingsUi.glossary.results', { count: searchResults.length, s: searchResults.length === 1 ? '' : 's' })}
|
|
<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">
|
|
{t('settingsUi.glossary.noMatch')}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 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">{t('settingsUi.glossary.tricksCount', { count: cat.tricks.length })}</span>
|
|
</div>
|
|
<p className="mb-5 text-sm text-gray-400">{cat.blurb}</p>
|
|
<div className="grid grid-cols-1 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">
|
|
{t('settingsUi.glossary.footerBefore')}{' '}
|
|
<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"
|
|
>
|
|
{t('settingsUi.glossary.sasquatch')}
|
|
</button>{' '}
|
|
{t('settingsUi.glossary.footerAfter')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|