|
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'; |
66 | 5 |
|
67 | 6 | 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(); |
228 | 8 |
|
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 | + } |
237 | 12 |
|
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 />; |
247 | 14 | }; |
248 | 15 |
|
249 | 16 | export default VocabPage; |
0 commit comments