Skip to content

Commit d5c468c

Browse files
committed
feat: new vocab pages
1 parent 8b68d9e commit d5c468c

File tree

3 files changed

+410
-242
lines changed

3 files changed

+410
-242
lines changed

src/pages/VocabPage.jsx

Lines changed: 9 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -1,249 +1,16 @@
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 BauhausShapes = ({ seed }) => {
15-
const shapes = useMemo(() => {
16-
// Simple deterministic RNG based on string seed
17-
let hash = 0;
18-
for (let i = 0; i < seed.length; i++) {
19-
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
20-
}
21-
22-
const rng = () => {
23-
const x = Math.sin(hash++) * 10000;
24-
return x - Math.floor(x);
25-
};
26-
27-
const count = 6;
28-
const items = [];
29-
const colors = ['#10b981', '#3b82f6', '#f87171', '#fb923c', '#ffffff'];
30-
31-
for (let i = 0; i < count; i++) {
32-
items.push({
33-
type: Math.floor(rng() * 4), // 0: rect, 1: circle, 2: triangle, 3: arc
34-
x: rng() * 100,
35-
y: rng() * 100,
36-
size: 15 + rng() * 25,
37-
rotation: Math.floor(rng() * 4) * 90,
38-
color: colors[Math.floor(rng() * colors.length)],
39-
opacity: 0.03 + rng() * 0.07
40-
});
41-
}
42-
return items;
43-
}, [seed]);
44-
45-
return (
46-
<svg className="absolute inset-0 w-full h-full pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
47-
{shapes.map((s, i) => (
48-
<g key={i} transform={`translate(${s.x}, ${s.y}) rotate(${s.rotation})`}>
49-
{s.type === 0 && (
50-
<rect x={-s.size/2} y={-s.size/2} width={s.size} height={s.size} fill={s.color} fillOpacity={s.opacity} />
51-
)}
52-
{s.type === 1 && (
53-
<circle cx={0} cy={0} r={s.size/2} fill={s.color} fillOpacity={s.opacity} />
54-
)}
55-
{s.type === 2 && (
56-
<path d={`M 0 ${-s.size/2} L ${s.size/2} ${s.size/2} L ${-s.size/2} ${s.size/2} Z`} fill={s.color} fillOpacity={s.opacity} />
57-
)}
58-
{s.type === 3 && (
59-
<path d={`M ${-s.size/2} ${-s.size/2} A ${s.size} ${s.size} 0 0 1 ${s.size/2} ${s.size/2} L ${-s.size/2} ${s.size/2} Z`} fill={s.color} fillOpacity={s.opacity} />
60-
)}
61-
</g>
62-
))}
63-
</svg>
64-
);
65-
};
1+
import React from 'react';
2+
import { useVisualSettings } from '../context/VisualSettingsContext';
3+
import BrutalistVocabPage from './brutalist-views/BrutalistVocabPage';
4+
import LuxeVocabPage from './luxe-views/LuxeVocabPage';
665

676
const VocabPage = () => {
68-
const [searchQuery, setSearchQuery] = useState('');
69-
const { openSidePanel } = useSidePanel();
70-
71-
const vocabEntries = useMemo(() =>
72-
Object.entries(vocabulary).map(([slug, data]) => ({
73-
slug,
74-
...data,
75-
})).sort((a, b) => a.title.localeCompare(b.title)),
76-
[]);
77-
78-
const filteredEntries = useMemo(() => {
79-
const query = searchQuery.toLowerCase().trim();
80-
if (!query) return vocabEntries;
81-
return vocabEntries.filter((entry) =>
82-
entry.title.toLowerCase().includes(query) ||
83-
entry.slug.toLowerCase().includes(query)
84-
);
85-
}, [vocabEntries, searchQuery]);
86-
87-
const groupedEntries = useMemo(() => {
88-
const groups = {};
89-
filteredEntries.forEach(entry => {
90-
const firstLetter = entry.title.charAt(0).toUpperCase();
91-
if (!groups[firstLetter]) groups[firstLetter] = [];
92-
groups[firstLetter].push(entry);
93-
});
94-
return groups;
95-
}, [filteredEntries]);
96-
97-
const alphabet = useMemo(() => Object.keys(groupedEntries).sort(), [groupedEntries]);
98-
99-
const handleOpenVocab = (entry) => {
100-
const LazyComponent = React.lazy(entry.loader);
101-
openSidePanel(entry.title, <LazyComponent />, 600);
102-
};
103-
104-
const scrollToLetter = (letter) => {
105-
const element = document.getElementById(`letter-${letter}`);
106-
if (element) {
107-
const offset = 140;
108-
const bodyRect = document.body.getBoundingClientRect().top;
109-
const elementRect = element.getBoundingClientRect().top;
110-
const elementPosition = elementRect - bodyRect;
111-
const offsetPosition = elementPosition - offset;
112-
113-
window.scrollTo({
114-
top: offsetPosition,
115-
behavior: 'smooth'
116-
});
117-
}
118-
};
119-
120-
return (
121-
<div className="min-h-screen bg-[#050505] text-[#f4f4f4] selection:bg-emerald-500/30 font-sans relative overflow-x-hidden">
122-
<Seo
123-
title="Glossary | Fezcodex"
124-
description="A dictionary of technical concepts and patterns."
125-
keywords={['Fezcodex', 'vocabulary', 'glossary', 'definitions']}
126-
/>
127-
128-
{/* Bauhaus Grid Background Pattern */}
129-
<div className="fixed inset-0 pointer-events-none opacity-[0.02] z-0"
130-
style={{
131-
backgroundImage: 'linear-gradient(#ffffff 1px, transparent 1px), linear-gradient(90deg, #ffffff 1px, transparent 1px)',
132-
backgroundSize: '40px 40px'
133-
}}
134-
/>
135-
136-
<div className="relative z-10 mx-auto max-w-7xl px-6 py-24 md:px-12 flex flex-col">
137-
{/* Header Section */}
138-
<header className="mb-24 flex flex-col items-start shrink-0">
139-
<Link
140-
to="/"
141-
className="mb-12 inline-flex items-center gap-3 text-md font-instr-sans text-gray-500 hover:text-white transition-colors"
142-
>
143-
<ArrowLeftIcon size={16} />
144-
<span>Fezcodex Index</span>
145-
</Link>
146-
147-
<h1 className="text-7xl md:text-9xl font-instr-serif italic tracking-tight mb-6 text-white leading-none">
148-
Glossary
149-
</h1>
150-
<p className="text-xl font-light text-gray-400 max-w-2xl font-instr-sans">
151-
A curated collection of technical concepts, design patterns, and terminology.
152-
</p>
153-
</header>
154-
155-
{/* Sticky Search & Nav */}
156-
<div className="sticky top-6 z-30 mb-20 shrink-0">
157-
<div className="bg-[#0a0a0a]/80 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl p-2 flex flex-col md:flex-row items-center gap-4">
158-
<div className="relative w-full md:w-96">
159-
<MagnifyingGlassIcon size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
160-
<input
161-
type="text"
162-
placeholder="Search terms..."
163-
value={searchQuery}
164-
onChange={(e) => setSearchQuery(e.target.value)}
165-
className="w-full bg-transparent text-lg font-instr-sans placeholder-gray-600 focus:outline-none py-3 pl-12 pr-4 text-white"
166-
/>
167-
{searchQuery && (
168-
<button onClick={() => setSearchQuery('')} className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-red-500">
169-
<XCircleIcon size={18} weight="fill" />
170-
</button>
171-
)}
172-
</div>
173-
174-
<div className="h-8 w-px bg-white/10 hidden md:block" />
175-
176-
<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">
177-
{alphabet.map(letter => (
178-
<button
179-
key={letter}
180-
onClick={() => scrollToLetter(letter)}
181-
className="w-8 h-8 flex items-center justify-center rounded-full text-xs font-bold text-gray-500 hover:bg-white hover:text-black transition-all font-mono"
182-
>
183-
{letter}
184-
</button>
185-
))}
186-
</div>
187-
</div>
188-
</div>
189-
190-
{/* Content Container - Shrinks with content */}
191-
<div className="flex-1 space-y-24 mb-32">
192-
{alphabet.map((letter) => (
193-
<motion.section
194-
key={letter}
195-
id={`letter-${letter}`}
196-
initial={{ opacity: 0 }}
197-
animate={{ opacity: 1 }}
198-
className="scroll-mt-40"
199-
>
200-
<div className="flex items-baseline gap-6 mb-10 border-b border-white/10 pb-4">
201-
<h2 className="text-6xl font-instr-serif italic text-white/20">{letter}</h2>
202-
</div>
203-
204-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
205-
{groupedEntries[letter].map((entry) => (
206-
<button
207-
key={entry.slug}
208-
onClick={() => handleOpenVocab(entry)}
209-
className="group flex flex-col text-left p-8 bg-[#0a0a0a] border border-white/5 hover:border-white/20 hover:shadow-2xl hover:shadow-emerald-900/10 transition-all duration-300 rounded-xl relative overflow-hidden h-full"
210-
>
211-
{/* Procedural Bauhaus Background */}
212-
<div className="absolute inset-0 opacity-40 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none">
213-
<BauhausShapes seed={entry.slug} />
214-
</div>
215-
216-
{/* Decorative Bauhaus Shape Overlay */}
217-
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl from-white/5 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
218-
219-
<div className="flex justify-between items-start w-full mb-6 relative z-10">
220-
<span className="font-mono text-[10px] text-gray-500 uppercase tracking-widest group-hover:text-emerald-400 transition-colors">
221-
{entry.slug}
222-
</span>
223-
<ArrowUpRightIcon
224-
size={18}
225-
className="text-gray-600 group-hover:text-white transition-colors"
226-
/>
227-
</div>
7+
const { fezcodexTheme } = useVisualSettings();
2288

229-
<h3 className="text-2xl font-instr-serif text-white mb-3 group-hover:underline decoration-1 underline-offset-4 decoration-white/30 relative z-10">
230-
{entry.title}
231-
</h3>
232-
</button>
233-
))}
234-
</div>
235-
</motion.section>
236-
))}
9+
if (fezcodexTheme === 'luxe') {
10+
return <LuxeVocabPage />;
11+
}
23712

238-
{filteredEntries.length === 0 && (
239-
<div className="py-32 text-center">
240-
<p className="font-instr-serif italic text-2xl text-gray-600">No definitions found for "{searchQuery}"</p>
241-
</div>
242-
)}
243-
</div>
244-
</div>
245-
</div>
246-
);
13+
return <BrutalistVocabPage />;
24714
};
24815

24916
export default VocabPage;

0 commit comments

Comments
 (0)