Skip to content

Commit 774756c

Browse files
committed
feat(terracotta): add TerracottaSeriesPage
1 parent 690039a commit 774756c

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useParams, Link } from 'react-router-dom';
3+
import { motion, AnimatePresence } from 'framer-motion';
4+
import TerracottaPostItem from '../../components/TerracottaPostItem';
5+
import {
6+
ArrowLeftIcon,
7+
TagIcon,
8+
BookOpenIcon,
9+
CalendarIcon,
10+
} from '@phosphor-icons/react';
11+
import Seo from '../../components/Seo';
12+
import { fetchAllBlogPosts } from '../../utils/dataUtils';
13+
import GenerativeArt from '../../components/GenerativeArt';
14+
15+
const SpecItem = ({ icon: Icon, label, value, isAccent }) => (
16+
<div className="flex flex-col gap-1">
17+
<span className="flex items-center gap-2 font-mono text-[9px] uppercase tracking-widest text-[#F3ECE0]/60">
18+
<Icon size={14} /> {label}
19+
</span>
20+
<span
21+
className={`font-mono text-sm uppercase ${isAccent ? 'text-[#C96442] font-bold' : 'text-[#F3ECE0]'}`}
22+
>
23+
{value}
24+
</span>
25+
</div>
26+
);
27+
28+
const TerracottaSeriesPage = () => {
29+
const { seriesSlug } = useParams();
30+
const [seriesPosts, setSeriesPosts] = useState([]);
31+
const [seriesTitle, setSeriesTitle] = useState('');
32+
const [loading, setLoading] = useState(true);
33+
const [activePost, setActivePost] = useState(null);
34+
35+
useEffect(() => {
36+
const fetchSeriesPosts = async () => {
37+
try {
38+
const { processedPosts } = await fetchAllBlogPosts();
39+
40+
const filteredPosts = processedPosts
41+
.filter((post) => post.series && post.series.slug === seriesSlug)
42+
.sort((a, b) => (a.seriesIndex || 0) - (b.seriesIndex || 0));
43+
44+
if (filteredPosts.length > 0) {
45+
setSeriesPosts(filteredPosts);
46+
setSeriesTitle(filteredPosts[0].series.title);
47+
setActivePost(filteredPosts[0]);
48+
} else {
49+
setSeriesPosts([]);
50+
setSeriesTitle('Series Not Found');
51+
}
52+
} catch (error) {
53+
console.error('Error fetching series posts:', error);
54+
setSeriesPosts([]);
55+
setSeriesTitle('Error');
56+
} finally {
57+
setLoading(false);
58+
}
59+
};
60+
61+
fetchSeriesPosts();
62+
}, [seriesSlug]);
63+
64+
const isPlaceholder = (post) => !post?.image || post.image.includes('placeholder');
65+
66+
if (loading) {
67+
return (
68+
<div className="flex h-screen items-center justify-center bg-[#F3ECE0] text-[#1A1613]">
69+
<div className="flex flex-col items-center gap-4">
70+
<div className="h-px w-24 bg-[#1A161320] relative overflow-hidden">
71+
<div className="absolute inset-0 bg-[#C96442] animate-progress origin-left"></div>
72+
</div>
73+
<span className="font-mono text-[10px] text-[#2E2620]/60 uppercase tracking-[0.3em]">
74+
Loading_Series
75+
</span>
76+
</div>
77+
</div>
78+
);
79+
}
80+
81+
return (
82+
<div className="flex min-h-screen bg-[#F3ECE0] text-[#1A1613] overflow-hidden relative selection:bg-[#C96442]/25">
83+
<Seo
84+
title={`${seriesTitle} | Fezcodex Series`}
85+
description={`Explore the sequential entries in the "${seriesTitle}" series.`}
86+
/>
87+
88+
<div className="absolute inset-0 opacity-20 pointer-events-none z-0">
89+
{activePost &&
90+
(isPlaceholder(activePost) ? (
91+
<GenerativeArt seed={activePost.title} className="w-full h-full filter blur-3xl" />
92+
) : (
93+
<img src={activePost.image} alt="bg" className="w-full h-full object-cover filter blur-3xl" />
94+
))}
95+
</div>
96+
97+
<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">
98+
<header className="mb-16">
99+
<Link
100+
to="/blog"
101+
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-[#2E2620]/60 hover:text-[#1A1613] transition-colors uppercase tracking-widest"
102+
>
103+
<ArrowLeftIcon weight="bold" />
104+
<span>Archive</span>
105+
</Link>
106+
<h1 className="text-6xl md:text-8xl font-playfairDisplay italic tracking-tight text-[#1A1613] mb-4 leading-none">
107+
Series
108+
</h1>
109+
<p className="text-[#2E2620]/60 font-mono text-[10px] uppercase tracking-[0.2em]">
110+
{'//'} SEQUENTIAL_ENTRIES: {seriesTitle.toUpperCase()}
111+
</p>
112+
</header>
113+
114+
<div className="flex flex-col pb-32">
115+
{seriesPosts.map((post) => (
116+
<TerracottaPostItem
117+
key={post.slug}
118+
post={{
119+
...post,
120+
slug: `series/${seriesSlug}/${post.slug}`,
121+
isSeries: false,
122+
}}
123+
isActive={activePost?.slug === post.slug}
124+
onHover={setActivePost}
125+
/>
126+
))}
127+
</div>
128+
129+
<div className="mt-auto pt-20 border-t border-[#1A161320] text-[#2E2620]/50 font-mono text-[10px] uppercase tracking-widest">
130+
Stored_Episodes: {seriesPosts.length}
131+
</div>
132+
</div>
133+
134+
<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">
135+
<AnimatePresence mode="wait">
136+
{activePost && (
137+
<motion.div
138+
key={activePost.slug}
139+
initial={{ opacity: 0 }}
140+
animate={{ opacity: 1 }}
141+
exit={{ opacity: 0 }}
142+
transition={{ duration: 0.4 }}
143+
className="absolute inset-0"
144+
>
145+
<div className="absolute inset-0 z-0">
146+
<GenerativeArt seed={activePost.title} className="w-full h-full opacity-60" />
147+
<div className="absolute inset-0 bg-gradient-to-t from-[#1A1613] via-transparent to-[#1A1613]/40" />
148+
</div>
149+
150+
<div className="absolute bottom-0 left-0 w-full p-16 z-10 flex flex-col gap-12">
151+
<div className="space-y-6 border-l border-[#F3ECE0]/10 pl-6">
152+
<SpecItem
153+
icon={CalendarIcon}
154+
label="Date"
155+
value={new Date(activePost.updated || activePost.date).toLocaleDateString('en-GB')}
156+
/>
157+
<SpecItem icon={TagIcon} label="Category" value={activePost.category || 'Episode'} isAccent />
158+
</div>
159+
160+
<div className="flex flex-col gap-4">
161+
<h2 className="text-4xl md:text-5xl font-playfairDisplay italic text-[#F3ECE0] tracking-tight leading-none">
162+
{activePost.title}
163+
</h2>
164+
<p className="text-lg text-[#F3ECE0]/80 leading-relaxed max-w-xl">
165+
{activePost.description || 'Part of a sequential series. Analysis and logs curated for review.'}
166+
</p>
167+
</div>
168+
169+
<motion.div
170+
initial={{ opacity: 0, y: 10 }}
171+
animate={{ opacity: 1, y: 0 }}
172+
transition={{ delay: 0.2 }}
173+
className="mt-4"
174+
>
175+
<Link
176+
to={`/blog/series/${seriesSlug}/${activePost.slug}`}
177+
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-1"
178+
>
179+
<span className="text-sm font-bold uppercase tracking-[0.2em]">Access_Episode</span>
180+
<BookOpenIcon weight="bold" size={20} />
181+
</Link>
182+
</motion.div>
183+
</div>
184+
</motion.div>
185+
)}
186+
</AnimatePresence>
187+
</div>
188+
</div>
189+
);
190+
};
191+
192+
export default TerracottaSeriesPage;

0 commit comments

Comments
 (0)