|
1 | | -import React, { useState, useEffect, useRef } from 'react'; |
2 | | -import { useParams, Link } from 'react-router-dom'; |
3 | | -import { |
4 | | - ArrowLeft, |
5 | | - CalendarBlank, |
6 | | - Tag, |
7 | | - Star, |
8 | | - ArrowUpRight, |
9 | | - Clock, |
10 | | - User, |
11 | | - Hash, |
12 | | -} from '@phosphor-icons/react'; |
13 | | -import GenerativeArt from '../components/GenerativeArt'; |
14 | | -import Seo from '../components/Seo'; |
15 | | -import piml from 'piml'; |
16 | | -import MarkdownLink from '../components/MarkdownLink'; |
17 | | -import colors from '../config/colors'; |
18 | | -import MarkdownContent from '../components/MarkdownContent'; |
19 | | -import CustomDropdown from '../components/CustomDropdown'; |
| 1 | +import React from 'react'; |
20 | 2 | import { useVisualSettings } from '../context/VisualSettingsContext'; |
| 3 | +import BrutalistLogDetailPage from './brutalist-views/BrutalistLogDetailPage'; |
| 4 | +import LuxeLogDetailPage from './luxe-views/LuxeLogDetailPage'; |
21 | 5 |
|
22 | 6 | const LogDetailPage = () => { |
23 | | - const { category, slugId } = useParams(); |
24 | | - const [log, setLog] = useState(null); |
25 | | - const [loading, setLoading] = useState(true); |
26 | | - const {headerFont, setHeaderFont, bodyFont, setBodyFont, availableFonts } = useVisualSettings(); |
27 | | - const contentRef = useRef(null); |
| 7 | + const { fezcodexTheme } = useVisualSettings(); |
28 | 8 |
|
29 | | - useEffect(() => { |
30 | | - const fetchLog = async () => { |
31 | | - setLoading(true); |
32 | | - try { |
33 | | - const response = await fetch(`/logs/${category}/${category}.piml`); |
34 | | - if (!response.ok) { |
35 | | - setLog({ attributes: { title: 'Category not found' }, body: '' }); |
36 | | - setLoading(false); |
37 | | - return; |
38 | | - } |
39 | | - const pimlText = await response.text(); |
40 | | - const data = piml.parse(pimlText); |
41 | | - const categoryLogs = data.logs || []; |
42 | | - const logMetadata = categoryLogs.find((item) => item.slug === slugId); |
43 | | - |
44 | | - if (logMetadata) { |
45 | | - try { |
46 | | - const logContentResponse = await fetch( |
47 | | - `/logs/${category}/${slugId}.txt`, |
48 | | - ); |
49 | | - if (logContentResponse.ok) { |
50 | | - const logBody = await logContentResponse.text(); |
51 | | - setLog({ attributes: logMetadata, body: logBody }); |
52 | | - } else { |
53 | | - // If text file is missing, just use metadata |
54 | | - setLog({ attributes: logMetadata, body: logMetadata.description || '' }); |
55 | | - } |
56 | | - } catch (e) { |
57 | | - setLog({ attributes: logMetadata, body: logMetadata.description || '' }); |
58 | | - } |
59 | | - } else { |
60 | | - setLog({ attributes: { title: 'Log not found' }, body: '' }); |
61 | | - } |
62 | | - } catch (error) { |
63 | | - setLog({ attributes: { title: 'Error loading log' }, body: '' }); |
64 | | - } |
65 | | - setLoading(false); |
66 | | - }; |
67 | | - fetchLog(); |
68 | | - }, [category, slugId]); |
69 | | - |
70 | | - if (loading) { |
71 | | - return ( |
72 | | - <div className="flex min-h-screen items-center justify-center bg-[#050505] text-white"> |
73 | | - <div className="flex flex-col items-center gap-4"> |
74 | | - <div className="h-px w-24 bg-white/10 relative overflow-hidden"> |
75 | | - <div className="absolute inset-0 bg-emerald-400 animate-progress origin-left"></div> |
76 | | - </div> |
77 | | - <span className="font-mono text-[10px] text-gray-500 uppercase tracking-widest"> |
78 | | - Accessing_Log |
79 | | - </span> |
80 | | - </div> |
81 | | - </div> |
82 | | - ); |
| 9 | + if (fezcodexTheme === 'luxe') { |
| 10 | + return <LuxeLogDetailPage />; |
83 | 11 | } |
84 | 12 |
|
85 | | - if (!log || !log.attributes.title) { |
86 | | - return ( |
87 | | - <div className="flex min-h-screen items-center justify-center bg-[#050505] text-white font-mono uppercase"> |
88 | | - 404 // Log Not Found |
89 | | - </div> |
90 | | - ); |
91 | | - } |
92 | | - |
93 | | - const { attributes, body } = log; |
94 | | - const accentColor = colors[category.toLowerCase()] || colors.primary[400]; |
95 | | - |
96 | | - const MetadataRow = ({ label, value, icon: Icon }) => { |
97 | | - if (!value) return null; |
98 | | - return ( |
99 | | - <div className="flex flex-col gap-1 py-4 border-b border-white/5"> |
100 | | - <span className="flex items-center gap-2 text-[10px] font-mono text-gray-500 uppercase tracking-widest"> |
101 | | - {Icon && ( |
102 | | - <Icon size={12} weight="bold" className="text-emerald-500" /> |
103 | | - )} |
104 | | - {label} |
105 | | - </span> |
106 | | - <span className="text-sm text-gray-200">{value}</span> |
107 | | - </div> |
108 | | - ); |
109 | | - }; |
110 | | - |
111 | | - const renderStars = (rating) => { |
112 | | - if (rating === undefined || rating === null) return null; |
113 | | - return ( |
114 | | - <div className="flex flex-col gap-2 py-4 border-b border-white/5"> |
115 | | - <span className="text-[10px] font-mono text-gray-500 uppercase tracking-widest flex items-center gap-2"> |
116 | | - <Star size={12} weight="bold" className="text-yellow-500" /> |
117 | | - Rating |
118 | | - </span> |
119 | | - <div className="flex gap-1"> |
120 | | - {[...Array(5)].map((_, i) => ( |
121 | | - <Star |
122 | | - key={i} |
123 | | - size={16} |
124 | | - weight="fill" |
125 | | - className={i < rating ? 'text-yellow-500' : 'text-white/10'} |
126 | | - /> |
127 | | - ))} |
128 | | - <span className="ml-2 font-mono text-xs text-gray-400"> |
129 | | - ({rating}/5) |
130 | | - </span> |
131 | | - </div> |
132 | | - </div> |
133 | | - ); |
134 | | - }; |
135 | | - |
136 | | - const fontMap = { |
137 | | - 'font-sans': "'Space Mono', monospace", |
138 | | - 'font-mono': "'JetBrains Mono', monospace", |
139 | | - 'font-inter': "'Inter', sans-serif", |
140 | | - 'font-arvo': "'Arvo', serif", |
141 | | - 'font-playfairDisplay': "'Playfair Display', serif", |
142 | | - 'font-syne': "'Syne', sans-serif", |
143 | | - 'font-outfit': "'Outfit', sans-serif", |
144 | | - 'font-ibm-plex-mono': "'IBM Plex Mono', monospace", |
145 | | - 'font-instr-serif': "'Instrument Serif', serif", |
146 | | - 'font-nunito': "'Nunito', sans-serif", |
147 | | - }; |
148 | | - |
149 | | - return ( |
150 | | - <div className={`min-h-screen bg-[#050505] text-white selection:bg-emerald-500/30`}> |
151 | | - <Seo |
152 | | - title={log ? `${log.attributes.title} | Fezcodex` : null} |
153 | | - description={log ? log.body.substring(0, 150) : null} |
154 | | - image={log?.attributes?.image} |
155 | | - /> |
156 | | - <style> |
157 | | - {` |
158 | | - .custom-prose h1, .custom-prose h2, .custom-prose h3, |
159 | | - .custom-prose h4, .custom-prose h5, .custom-prose h6 { |
160 | | - font-family: ${fontMap[headerFont] || fontMap['font-sans']} !important; |
161 | | - } |
162 | | - `} |
163 | | - </style> |
164 | | - {/* HERO SECTION */} |
165 | | - <section className="relative h-[60vh] md:h-[70vh] flex flex-col justify-end overflow-hidden border-b border-white/10"> |
166 | | - <div className="absolute inset-0 z-0"> |
167 | | - <GenerativeArt |
168 | | - seed={attributes.title} |
169 | | - className="w-full h-full opacity-60" |
170 | | - /> |
171 | | - <div className="absolute inset-0 bg-gradient-to-t from-[#050505] via-[#050505]/60 to-transparent" /> |
172 | | - <div className="absolute inset-0 bg-noise opacity-[0.05] mix-blend-overlay" /> |
173 | | - </div> |
174 | | - |
175 | | - <div className="relative z-10 mx-auto max-w-7xl w-full px-6 pb-20"> |
176 | | - <Link |
177 | | - to="/logs" |
178 | | - className="group mb-12 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-widest" |
179 | | - > |
180 | | - <ArrowLeft |
181 | | - weight="bold" |
182 | | - className="group-hover:-translate-x-1 transition-transform" |
183 | | - /> |
184 | | - <span>Archive</span> |
185 | | - </Link> |
186 | | - |
187 | | - <div className="flex flex-wrap items-center gap-4 mb-8"> |
188 | | - <span |
189 | | - className="px-3 py-1 text-[10px] font-mono font-bold uppercase tracking-widest border rounded-full bg-opacity-20 backdrop-blur-md" |
190 | | - style={{ |
191 | | - color: accentColor, |
192 | | - borderColor: `${accentColor}44`, |
193 | | - backgroundColor: `${accentColor}10`, |
194 | | - }} |
195 | | - > |
196 | | - {category} |
197 | | - </span> |
198 | | - {attributes.updated && ( |
199 | | - <span className="px-3 py-1 text-[10px] font-mono font-bold uppercase tracking-widest text-rose-400 border border-rose-400/20 rounded-full bg-rose-400/5"> |
200 | | - Updated |
201 | | - </span> |
202 | | - )} |
203 | | - </div> |
204 | | - |
205 | | - <h1 className="text-5xl md:text-8xl font-black uppercase tracking-tighter leading-none mb-8 max-w-4xl"> |
206 | | - {attributes.title} |
207 | | - </h1> |
208 | | - |
209 | | - <div className="flex flex-wrap items-center gap-8 text-gray-400 font-mono text-[10px] uppercase tracking-[0.2em]"> |
210 | | - <div className="flex items-center gap-2"> |
211 | | - <CalendarBlank size={16} className="text-emerald-500" /> |
212 | | - <span>Published: {attributes.date}</span> |
213 | | - </div> |
214 | | - {attributes.slug && ( |
215 | | - <div className="flex items-center gap-2"> |
216 | | - <Hash size={16} className="text-emerald-500" /> |
217 | | - <span>ID: {attributes.slug}</span> |
218 | | - </div> |
219 | | - )} |
220 | | - </div> |
221 | | - </div> |
222 | | - </section> |
223 | | - |
224 | | - {/* CONTENT SECTION */} |
225 | | - <div className="mx-auto max-w-7xl px-6 py-20 lg:py-32"> |
226 | | - <div className="grid grid-cols-1 lg:grid-cols-12 gap-16 md:gap-24"> |
227 | | - {/* Main Reading Column */} |
228 | | - <main className="lg:col-span-8"> |
229 | | - <article |
230 | | - ref={contentRef} |
231 | | - className={`prose prose-xl prose-dark prose-emerald max-w-none custom-prose ${bodyFont} |
232 | | - prose-headings:uppercase prose-headings:tracking-tighter prose-headings:font-black |
233 | | - prose-p:text-gray-300 prose-p:leading-relaxed |
234 | | - prose-a:text-emerald-400 prose-a:no-underline hover:prose-a:underline |
235 | | - prose-blockquote:border-l-4 prose-blockquote:border-emerald-500 prose-blockquote:bg-white/5 prose-blockquote:py-1 prose-blockquote:px-6 |
236 | | - prose-strong:text-white prose-code:text-emerald-300 prose-code:bg-emerald-500/10 prose-code:px-1 prose-code:rounded-sm`} |
237 | | - > |
238 | | - <MarkdownContent |
239 | | - content={body} |
240 | | - components={{ |
241 | | - a: (props) => ( |
242 | | - <MarkdownLink |
243 | | - {...props} |
244 | | - className="text-emerald-400 hover:text-white transition-colors underline decoration-emerald-500/30 underline-offset-4" |
245 | | - /> |
246 | | - ), |
247 | | - }} |
248 | | - /> |
249 | | - </article> |
250 | | - |
251 | | - {/* Tags Section */} |
252 | | - {attributes.tags && attributes.tags.length > 0 && ( |
253 | | - <div className="mt-20 pt-12 border-t border-white/10"> |
254 | | - <div className="flex flex-wrap gap-2"> |
255 | | - {attributes.tags.map((tag) => ( |
256 | | - <span |
257 | | - key={tag} |
258 | | - className="px-3 py-1 bg-white/5 border border-white/10 rounded-full text-[10px] font-mono text-gray-400 uppercase tracking-widest hover:text-white hover:border-white transition-colors cursor-default" |
259 | | - > |
260 | | - #{tag} |
261 | | - </span> |
262 | | - ))} |
263 | | - </div> |
264 | | - </div> |
265 | | - )} |
266 | | - </main> |
267 | | - |
268 | | - {/* Sidebar Metadata */} |
269 | | - <aside className="lg:col-span-4"> |
270 | | - <div className="sticky top-24 space-y-12"> |
271 | | - <div className="border border-white/10 p-8 bg-white/[0.02] backdrop-blur-sm rounded-sm relative overflow-hidden group"> |
272 | | - {/* Background accent line */} |
273 | | - <div className="absolute top-0 left-0 w-1 h-0 group-hover:h-full bg-emerald-500 transition-all duration-500" /> |
274 | | - <h3 className="font-mono text-[10px] font-bold text-emerald-500 uppercase tracking-widest mb-8 flex items-center gap-2"> |
275 | | - <Tag weight="fill" /> |
276 | | - Manifest Data |
277 | | - </h3> |
278 | | - <div className="flex flex-col"> |
279 | | - <MetadataRow |
280 | | - label="Category" |
281 | | - value={attributes.category} |
282 | | - icon={Tag} |
283 | | - /> |
284 | | - <MetadataRow |
285 | | - label="Creator/Author" |
286 | | - value={ |
287 | | - attributes.author || |
288 | | - attributes.director || |
289 | | - attributes.artist || |
290 | | - attributes.creator || |
291 | | - attributes.by |
292 | | - } |
293 | | - icon={User} |
294 | | - /> |
295 | | - <MetadataRow |
296 | | - label="Platform/Source" |
297 | | - value={attributes.platform || attributes.source} |
298 | | - icon={ArrowUpRight} |
299 | | - /> |
300 | | - <MetadataRow |
301 | | - label="Release Year" |
302 | | - value={attributes.year} |
303 | | - icon={Clock} |
304 | | - /> |
305 | | - <MetadataRow |
306 | | - label="Last Updated" |
307 | | - value={attributes.updated} |
308 | | - icon={CalendarBlank} |
309 | | - /> |
310 | | - {renderStars(attributes.rating)} |
311 | | - </div> |
312 | | - {attributes.link && ( |
313 | | - <a |
314 | | - href={attributes.link} |
315 | | - target="_blank" |
316 | | - rel="noopener noreferrer" |
317 | | - className="mt-8 flex items-center justify-start gap-3 group/link border border-emerald-500/20 hover:border-emerald-500 p-4 transition-all" |
318 | | - > |
319 | | - <span className="text-xs font-mono uppercase tracking-widest text-emerald-400 group-hover/link:text-emerald-500 transition-colors"> |
320 | | - Access External Source |
321 | | - </span> |
322 | | - <ArrowUpRight |
323 | | - className="text-emerald-400 transition-transform group-hover/link:translate-x-1 group-hover/link:-translate-y-1" |
324 | | - size={20} |
325 | | - /> |
326 | | - </a> |
327 | | - )}{' '} |
328 | | - </div> |
329 | | - |
330 | | - {/* Typography Lab */} |
331 | | - <div className="border border-white/10 p-8 bg-white/[0.02] backdrop-blur-sm rounded-sm relative overflow-hidden group"> |
332 | | - <div className="absolute top-0 left-0 w-1 h-0 group-hover:h-full bg-emerald-500 transition-all duration-500" /> |
333 | | - <h3 className="font-mono text-[10px] font-bold text-emerald-500 uppercase tracking-widest mb-8 flex items-center gap-2"> |
334 | | - <Tag weight="fill" /> |
335 | | - Typography Lab |
336 | | - </h3> |
337 | | - <div className="space-y-6"> |
338 | | - <div className="space-y-2"> |
339 | | - <span className="text-[9px] font-mono text-gray-500 uppercase tracking-widest block ml-1">Header Font</span> |
340 | | - <CustomDropdown |
341 | | - label="Select Font" |
342 | | - options={availableFonts.map((f) => ({ |
343 | | - label: f.name, |
344 | | - value: f.id, |
345 | | - }))} |
346 | | - value={headerFont} |
347 | | - onChange={setHeaderFont} |
348 | | - variant="brutalist" |
349 | | - fullWidth={true} |
350 | | - /> |
351 | | - </div> |
352 | | - <div className="space-y-2"> |
353 | | - <span className="text-[9px] font-mono text-gray-500 uppercase tracking-widest block ml-1">Body Font</span> |
354 | | - <CustomDropdown |
355 | | - label="Select Font" |
356 | | - options={availableFonts.map((f) => ({ |
357 | | - label: f.name, |
358 | | - value: f.id, |
359 | | - }))} |
360 | | - value={bodyFont} |
361 | | - onChange={setBodyFont} |
362 | | - variant="brutalist" |
363 | | - fullWidth={true} |
364 | | - /> |
365 | | - </div> |
366 | | - </div> |
367 | | - <div className="mt-8 pt-4 border-t border-white/5 text-[9px] font-mono text-gray-600 uppercase tracking-widest flex justify-between"> |
368 | | - <span>You can also change them in Settings page</span> |
369 | | - </div> |
370 | | - </div> |
371 | | - |
372 | | - {/* Optional: Read More / Next Prev could go here */} |
373 | | - </div> |
374 | | - </aside> |
375 | | - </div> |
376 | | - </div> |
377 | | - </div> |
378 | | - ); |
| 13 | + return <BrutalistLogDetailPage />; |
379 | 14 | }; |
380 | 15 |
|
381 | 16 | export default LogDetailPage; |
0 commit comments