Skip to content

Commit 088936b

Browse files
committed
feat(terracotta): add TerracottaBlogPage
1 parent f73ed28 commit 088936b

1 file changed

Lines changed: 305 additions & 0 deletions

File tree

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import TerracottaPostItem from '../../components/TerracottaPostItem';
5+
import GenerativeArt from '../../components/GenerativeArt';
6+
import Seo from '../../components/Seo';
7+
import { fetchAllBlogPosts } from '../../utils/dataUtils';
8+
import {
9+
ArrowLeft,
10+
XCircle,
11+
Clock,
12+
Tag,
13+
BookOpen,
14+
MagnifyingGlass,
15+
Hash,
16+
} from '@phosphor-icons/react';
17+
18+
const FILTERS = [
19+
{ id: 'all', label: 'All' },
20+
{ id: 'dev', label: 'Dev' },
21+
{ id: 'ai', label: 'AI' },
22+
{ id: 'feat', label: 'Feat' },
23+
{ id: 'rant', label: 'Rant' },
24+
{ id: 'series', label: 'Series' },
25+
{ id: 'gist', label: 'Gist' },
26+
{ id: 'd&d', label: 'D&D' },
27+
];
28+
29+
const TerracottaBlogPage = () => {
30+
const [displayItems, setDisplayItems] = useState([]);
31+
const [loading, setLoading] = useState(true);
32+
const [activeFilter, setActiveFilter] = useState('all');
33+
const [searchQuery, setSearchQuery] = useState('');
34+
const [activePost, setActivePost] = useState(null);
35+
36+
useEffect(() => {
37+
const fetchPosts = async () => {
38+
try {
39+
const { processedPosts } = await fetchAllBlogPosts();
40+
41+
const seriesMap = new Map();
42+
const individualPosts = [];
43+
44+
processedPosts.forEach((post) => {
45+
if (post.series) {
46+
if (!seriesMap.has(post.series.slug)) {
47+
seriesMap.set(post.series.slug, {
48+
title: post.series.title,
49+
slug: post.series.slug,
50+
date: post.series.date || post.date,
51+
updated: post.series.updated || post.updated,
52+
image: post.series.image,
53+
isSeries: true,
54+
posts: [],
55+
tags: post.tags,
56+
category: post.category,
57+
description: post.series.description || post.description,
58+
});
59+
}
60+
seriesMap.get(post.series.slug).posts.push(post);
61+
} else {
62+
individualPosts.push(post);
63+
}
64+
});
65+
66+
const combinedItems = [...Array.from(seriesMap.values()), ...individualPosts];
67+
combinedItems.sort(
68+
(a, b) => new Date(b.updated || b.date) - new Date(a.updated || a.date),
69+
);
70+
71+
setDisplayItems(combinedItems);
72+
if (combinedItems.length > 0) setActivePost(combinedItems[0]);
73+
} catch (error) {
74+
console.error('Error fetching blog data:', error);
75+
} finally {
76+
setLoading(false);
77+
}
78+
};
79+
fetchPosts();
80+
}, []);
81+
82+
const filteredItems = displayItems.filter((item) => {
83+
const matchesFilter = () => {
84+
if (activeFilter === 'all') return true;
85+
if (activeFilter === 'series') return item.isSeries;
86+
return item.category === activeFilter;
87+
};
88+
const matchesSearch = () => {
89+
if (!searchQuery) return true;
90+
const q = searchQuery.toLowerCase();
91+
return item.title?.toLowerCase().includes(q) || item.slug?.toLowerCase().includes(q);
92+
};
93+
return matchesFilter() && matchesSearch();
94+
});
95+
96+
const isPlaceholder = (post) => !post?.image || post.image.includes('placeholder');
97+
98+
if (loading) {
99+
return (
100+
<div className="flex h-screen items-center justify-center bg-[#F3ECE0] text-[#1A1613]">
101+
<div className="flex flex-col items-center gap-4">
102+
<div className="h-px w-24 bg-[#1A161320] relative overflow-hidden">
103+
<div className="absolute inset-0 bg-[#C96442] animate-progress origin-left"></div>
104+
</div>
105+
<span className="font-mono text-[10px] text-[#2E2620]/60 uppercase tracking-[0.3em]">
106+
Loading_Archive
107+
</span>
108+
</div>
109+
</div>
110+
);
111+
}
112+
113+
return (
114+
<div className="flex min-h-screen bg-[#F3ECE0] text-[#1A1613] overflow-hidden relative selection:bg-[#C96442]/25">
115+
<Seo title="Archive | Fezcodex Blog" description="Curated thoughts, insights, and digital rants." />
116+
117+
<div className="absolute inset-0 opacity-20 pointer-events-none z-0">
118+
{activePost &&
119+
(isPlaceholder(activePost) ? (
120+
<GenerativeArt seed={activePost.title} className="w-full h-full filter blur-3xl" />
121+
) : (
122+
<img src={activePost.image} alt="bg" className="w-full h-full object-cover filter blur-3xl" />
123+
))}
124+
</div>
125+
126+
<div className="w-full 4xl:pr-[50vw] relative z-10 flex flex-col min-h-screen py-24 px-6 md:pl-20 overflow-y-auto overflow-x-hidden no-scrollbar transition-all duration-300">
127+
<header className="mb-16">
128+
<Link
129+
to="/"
130+
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-[#2E2620]/60 hover:text-[#1A1613] transition-colors uppercase tracking-widest"
131+
>
132+
<ArrowLeft weight="bold" />
133+
<span>Home</span>
134+
</Link>
135+
<h1 className="text-6xl md:text-8xl font-playfairDisplay italic tracking-tight text-[#1A1613] mb-4 leading-none">
136+
Intel
137+
</h1>
138+
<p className="text-[#2E2620]/60 font-mono text-[10px] uppercase tracking-[0.2em]">
139+
{'//'} DATA_LOGS & ARCHIVED_THOUGHTS
140+
</p>
141+
</header>
142+
143+
<div className="mb-12 border-b border-[#1A161320] pb-8 space-y-6">
144+
<div className="flex flex-wrap items-center gap-2">
145+
{FILTERS.map((f) => (
146+
<button
147+
key={f.id}
148+
onClick={() => setActiveFilter(f.id)}
149+
className={`rounded-sm px-3 py-1 text-[10px] font-bold uppercase tracking-widest transition-all ${
150+
activeFilter === f.id
151+
? 'bg-[#C96442] text-[#F3ECE0] shadow-[0_10px_20px_-10px_#C9644250]'
152+
: 'bg-[#E8DECE]/50 text-[#2E2620]/60 hover:text-[#1A1613] hover:bg-[#E8DECE] border border-[#1A161320]'
153+
}`}
154+
>
155+
{f.label}
156+
</button>
157+
))}
158+
</div>
159+
160+
<div className="relative group w-full max-w-md">
161+
<div className="flex items-center gap-2 bg-[#E8DECE]/50 border border-[#1A161320] rounded-sm px-3 py-1.5 focus-within:border-[#C96442]/60 focus-within:bg-[#E8DECE] transition-all">
162+
<MagnifyingGlass
163+
size={14}
164+
className="text-[#2E2620]/50 group-focus-within:text-[#9E4A2F]"
165+
/>
166+
<input
167+
type="text"
168+
value={searchQuery}
169+
onChange={(e) => setSearchQuery(e.target.value)}
170+
placeholder="Search Blogposts..."
171+
className="bg-transparent border-none outline-none text-[10px] font-mono uppercase tracking-widest text-[#1A1613] placeholder-[#2E2620]/30 w-full"
172+
/>
173+
{searchQuery && (
174+
<button
175+
onClick={() => setSearchQuery('')}
176+
className="text-[#2E2620]/50 hover:text-[#9E4A2F] transition-colors"
177+
>
178+
<XCircle size={14} weight="fill" />
179+
</button>
180+
)}
181+
</div>
182+
</div>
183+
</div>
184+
185+
<div className="flex flex-col pb-32">
186+
{filteredItems.map((item) => (
187+
<TerracottaPostItem
188+
key={item.slug}
189+
post={item}
190+
isActive={activePost?.slug === item.slug}
191+
onHover={setActivePost}
192+
/>
193+
))}
194+
{filteredItems.length === 0 && (
195+
<div className="py-12 text-center border border-dashed border-[#1A161320] rounded-sm">
196+
<p className="font-mono text-xs text-[#2E2620]/50 uppercase tracking-widest">
197+
No_Intel_Found
198+
</p>
199+
</div>
200+
)}
201+
</div>
202+
203+
<div className="mt-auto pt-20 border-t border-[#1A161320] text-[#2E2620]/50 font-mono text-[10px] uppercase tracking-widest">
204+
Total Stored Entries: {displayItems.length}
205+
</div>
206+
</div>
207+
208+
<div className="hidden 4xl:block fixed right-0 top-0 h-screen w-1/2 bg-[#1A1613] overflow-hidden border-l border-[#1A161320] z-20">
209+
<AnimatePresence mode="wait">
210+
{activePost && (
211+
<motion.div
212+
key={activePost.slug}
213+
initial={{ opacity: 0 }}
214+
animate={{ opacity: 1 }}
215+
exit={{ opacity: 0 }}
216+
transition={{ duration: 0.4 }}
217+
className="absolute inset-0"
218+
>
219+
<div className="absolute inset-0 z-0">
220+
<GenerativeArt seed={activePost.title} className="w-full h-full opacity-60" />
221+
<div className="absolute inset-0 bg-gradient-to-t from-[#1A1613] via-transparent to-[#1A1613]/40" />
222+
</div>
223+
224+
<div className="absolute bottom-0 left-0 w-full p-16 z-10 flex flex-col gap-8">
225+
<div className="flex items-center gap-6">
226+
<div className="flex items-center gap-2 text-[#C96442] font-mono text-[10px] tracking-widest uppercase">
227+
<Clock size={16} />
228+
<span>{new Date(activePost.updated || activePost.date).toLocaleDateString('en-GB')}</span>
229+
</div>
230+
<div className="flex items-center gap-2 text-[#F3ECE0] font-mono text-[10px] tracking-widest uppercase bg-[#F3ECE0]/10 px-2 py-1 border border-[#F3ECE0]/10 rounded-sm">
231+
<Tag size={14} />
232+
<span>{activePost.category || 'Post'}</span>
233+
</div>
234+
</div>
235+
236+
<div className="flex flex-col gap-4">
237+
<h2 className="text-6xl md:text-7xl font-playfairDisplay italic text-[#F3ECE0] tracking-tight leading-none">
238+
{activePost.title}
239+
</h2>
240+
<p className="text-lg text-[#F3ECE0]/80 leading-relaxed max-w-xl">
241+
{activePost.description || 'Archived content from the digital vault.'}
242+
</p>
243+
</div>
244+
245+
{activePost.tags && activePost.tags.length > 0 && (
246+
<div className="flex flex-wrap gap-2 max-w-xl">
247+
{activePost.tags.map((tag) => (
248+
<span
249+
key={tag}
250+
className="inline-flex items-center gap-1 text-[9px] font-mono font-bold uppercase tracking-wider text-[#F3ECE0]/70 bg-[#1A1613]/60 px-2 py-1 rounded-sm border border-[#F3ECE0]/5"
251+
>
252+
<Hash size={10} /> {tag}
253+
</span>
254+
))}
255+
</div>
256+
)}
257+
258+
{activePost.isSeries && (
259+
<div className="mt-4 flex flex-col gap-4">
260+
<span className="font-mono text-[10px] text-[#C96442] font-bold tracking-widest uppercase">
261+
{'//'} SERIES_MANIFEST
262+
</span>
263+
<div className="grid grid-cols-1 gap-2">
264+
{activePost.posts?.slice(0, 3).map((p, i) => (
265+
<div
266+
key={p.slug}
267+
className="flex items-center gap-3 text-[#F3ECE0]/60 font-mono text-[10px] uppercase"
268+
>
269+
<span>{String(i + 1).padStart(2, '0')}</span>
270+
<span className="h-px w-4 bg-[#F3ECE0]/20" />
271+
<span className="truncate">{p.title}</span>
272+
</div>
273+
))}
274+
</div>
275+
</div>
276+
)}
277+
278+
<motion.div
279+
initial={{ opacity: 0, y: 10 }}
280+
animate={{ opacity: 1, y: 0 }}
281+
transition={{ delay: 0.2 }}
282+
className="mt-8"
283+
>
284+
<Link
285+
to={
286+
activePost.isSeries
287+
? `/blog/series/${activePost.slug}`
288+
: `/blog/${activePost.slug}`
289+
}
290+
className="inline-flex items-center gap-4 text-[#F3ECE0] border-b-2 border-[#C96442] pb-2 hover:bg-[#C96442] hover:text-[#F3ECE0] transition-all px-2 py-2"
291+
>
292+
<span className="text-sm uppercase tracking-[0.2em]">Read Post</span>
293+
<BookOpen weight="bold" size={20} />
294+
</Link>
295+
</motion.div>
296+
</div>
297+
</motion.div>
298+
)}
299+
</AnimatePresence>
300+
</div>
301+
</div>
302+
);
303+
};
304+
305+
export default TerracottaBlogPage;

0 commit comments

Comments
 (0)