Skip to content

Commit fffc3f4

Browse files
committed
feat(terracotta): add TerracottaLogDetailPage
1 parent 2b6458d commit fffc3f4

1 file changed

Lines changed: 347 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)