Skip to content

Commit 23cb5b5

Browse files
committed
feat(terracotta): add TerracottaVocabPage
1 parent 3155842 commit 23cb5b5

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { useState, useMemo } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
MagnifyingGlassIcon,
6+
XCircleIcon,
7+
ArrowUpRightIcon,
8+
} from '@phosphor-icons/react';
9+
import { motion } from 'framer-motion';
10+
import { vocabulary } from '../../data/vocabulary';
11+
import Seo from '../../components/Seo';
12+
import { useSidePanel } from '../../context/SidePanelContext';
13+
14+
const TerracottaVocabPage = () => {
15+
const [searchQuery, setSearchQuery] = useState('');
16+
const { openSidePanel } = useSidePanel();
17+
18+
const vocabEntries = useMemo(
19+
() =>
20+
Object.entries(vocabulary)
21+
.map(([slug, data]) => ({ slug, ...data }))
22+
.sort((a, b) => a.title.localeCompare(b.title)),
23+
[],
24+
);
25+
26+
const filteredEntries = useMemo(() => {
27+
const query = searchQuery.toLowerCase().trim();
28+
if (!query) return vocabEntries;
29+
return vocabEntries.filter(
30+
(entry) =>
31+
entry.title.toLowerCase().includes(query) ||
32+
entry.slug.toLowerCase().includes(query),
33+
);
34+
}, [vocabEntries, searchQuery]);
35+
36+
const groupedEntries = useMemo(() => {
37+
const groups = {};
38+
filteredEntries.forEach((entry) => {
39+
const firstLetter = entry.title.charAt(0).toUpperCase();
40+
if (!groups[firstLetter]) groups[firstLetter] = [];
41+
groups[firstLetter].push(entry);
42+
});
43+
return groups;
44+
}, [filteredEntries]);
45+
46+
const alphabet = useMemo(() => Object.keys(groupedEntries).sort(), [groupedEntries]);
47+
48+
const handleOpenVocab = (entry) => {
49+
const LazyComponent = React.lazy(entry.loader);
50+
openSidePanel(entry.title, <LazyComponent />, 600);
51+
};
52+
53+
const scrollToLetter = (letter) => {
54+
const element = document.getElementById(`letter-${letter}`);
55+
if (element) {
56+
const offset = 140;
57+
const bodyRect = document.body.getBoundingClientRect().top;
58+
const elementRect = element.getBoundingClientRect().top;
59+
const elementPosition = elementRect - bodyRect;
60+
window.scrollTo({ top: elementPosition - offset, behavior: 'smooth' });
61+
}
62+
};
63+
64+
return (
65+
<div className="min-h-screen bg-[#F3ECE0] text-[#1A1613] selection:bg-[#C96442]/25 font-playfairDisplay relative overflow-x-hidden">
66+
<Seo title="Glossary | Fezcodex" description="A dictionary of technical concepts and patterns." />
67+
68+
<div
69+
className="fixed inset-0 pointer-events-none opacity-[0.04] z-0"
70+
style={{
71+
backgroundImage: 'linear-gradient(#1A1613 1px, transparent 1px), linear-gradient(90deg, #1A1613 1px, transparent 1px)',
72+
backgroundSize: '40px 40px',
73+
}}
74+
/>
75+
76+
<div className="relative z-10 mx-auto max-w-7xl px-6 py-24 md:px-12 flex flex-col">
77+
<header className="mb-24 flex flex-col items-start shrink-0">
78+
<Link
79+
to="/"
80+
className="mb-12 inline-flex items-center gap-3 text-md text-[#2E2620]/60 hover:text-[#1A1613] transition-colors font-mono uppercase tracking-widest text-xs"
81+
>
82+
<ArrowLeftIcon size={16} />
83+
<span>Fezcodex Index</span>
84+
</Link>
85+
86+
<h1 className="text-7xl md:text-9xl font-playfairDisplay italic tracking-tight mb-6 text-[#1A1613] leading-none">
87+
Glossary
88+
</h1>
89+
<p className="text-xl text-[#2E2620] max-w-2xl">
90+
A curated collection of technical concepts, design patterns, and terminology.
91+
</p>
92+
</header>
93+
94+
<div className="sticky top-6 z-30 mb-20 shrink-0">
95+
<div className="bg-[#F3ECE0]/90 backdrop-blur-xl border border-[#1A161320] shadow-[0_20px_40px_-25px_#1A161330] rounded-sm p-2 flex flex-col md:flex-row items-center gap-4">
96+
<div className="relative w-full md:w-96">
97+
<MagnifyingGlassIcon size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-[#2E2620]/50" />
98+
<input
99+
type="text"
100+
placeholder="Search terms..."
101+
value={searchQuery}
102+
onChange={(e) => setSearchQuery(e.target.value)}
103+
className="w-full bg-transparent text-lg placeholder-[#2E2620]/40 focus:outline-none py-3 pl-12 pr-4 text-[#1A1613]"
104+
/>
105+
{searchQuery && (
106+
<button
107+
onClick={() => setSearchQuery('')}
108+
className="absolute right-4 top-1/2 -translate-y-1/2 text-[#2E2620]/50 hover:text-[#9E4A2F]"
109+
>
110+
<XCircleIcon size={18} weight="fill" />
111+
</button>
112+
)}
113+
</div>
114+
115+
<div className="h-8 w-px bg-[#1A161320] hidden md:block" />
116+
117+
<div className="flex flex-wrap gap-1 justify-center md:justify-start px-2 py-2 md:py-0 w-full overflow-x-auto no-scrollbar">
118+
{alphabet.map((letter) => (
119+
<button
120+
key={letter}
121+
onClick={() => scrollToLetter(letter)}
122+
className="w-8 h-8 flex items-center justify-center rounded-full text-xs font-bold text-[#2E2620]/50 hover:bg-[#1A1613] hover:text-[#F3ECE0] transition-all font-mono"
123+
>
124+
{letter}
125+
</button>
126+
))}
127+
</div>
128+
</div>
129+
</div>
130+
131+
<div className="flex-1 space-y-24 mb-32">
132+
{alphabet.map((letter) => (
133+
<motion.section
134+
key={letter}
135+
id={`letter-${letter}`}
136+
initial={{ opacity: 0 }}
137+
animate={{ opacity: 1 }}
138+
className="scroll-mt-40"
139+
>
140+
<div className="flex items-baseline gap-6 mb-10 border-b border-[#1A161320] pb-4">
141+
<h2 className="text-6xl font-playfairDisplay italic text-[#1A161320]">{letter}</h2>
142+
</div>
143+
144+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
145+
{groupedEntries[letter].map((entry) => (
146+
<button
147+
key={entry.slug}
148+
onClick={() => handleOpenVocab(entry)}
149+
className="group flex flex-col text-left p-8 bg-[#F3ECE0] border border-[#1A161320] hover:border-[#1A1613] hover:shadow-[0_20px_40px_-25px_#1A161330] transition-all duration-300 rounded-sm relative overflow-hidden h-full"
150+
>
151+
<div className="flex justify-between items-start w-full mb-6 relative z-10">
152+
<span className="font-mono text-[10px] text-[#2E2620]/60 uppercase tracking-widest group-hover:text-[#9E4A2F] transition-colors">
153+
{entry.slug}
154+
</span>
155+
<ArrowUpRightIcon size={18} className="text-[#2E2620]/40 group-hover:text-[#1A1613] transition-colors" />
156+
</div>
157+
158+
<h3 className="text-2xl font-playfairDisplay italic text-[#1A1613] mb-3 group-hover:underline decoration-1 underline-offset-4 decoration-[#C96442] relative z-10">
159+
{entry.title}
160+
</h3>
161+
</button>
162+
))}
163+
</div>
164+
</motion.section>
165+
))}
166+
167+
{filteredEntries.length === 0 && (
168+
<div className="py-32 text-center">
169+
<p className="font-playfairDisplay italic text-2xl text-[#2E2620]/40">
170+
No definitions found for "{searchQuery}"
171+
</p>
172+
</div>
173+
)}
174+
</div>
175+
</div>
176+
</div>
177+
);
178+
};
179+
180+
export default TerracottaVocabPage;

0 commit comments

Comments
 (0)