Skip to content

Commit ddfc47f

Browse files
committed
feat: better blog pages
1 parent 7c5d6c2 commit ddfc47f

File tree

8 files changed

+1492
-343
lines changed

8 files changed

+1492
-343
lines changed

src/pages/BlogPage.jsx

Lines changed: 9 additions & 329 deletions
Original file line numberDiff line numberDiff line change
@@ -1,336 +1,16 @@
1-
import React, { useState, useEffect } from 'react';
2-
import { Link } from 'react-router-dom';
3-
import { motion, AnimatePresence } from 'framer-motion';
4-
import PostItem from '../components/PostItem';
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: 'feat', label: 'Feat' },
22-
{ id: 'rant', label: 'Rant' },
23-
{ id: 'series', label: 'Series' },
24-
{ id: 'gist', label: 'Gist' },
25-
{ id: 'd&d', label: 'D&D' },
26-
];
1+
import React from 'react';
2+
import { useVisualSettings } from '../context/VisualSettingsContext';
3+
import BrutalistBlogPage from './blog-views/BrutalistBlogPage';
4+
import LuxeBlogPage from './luxe-views/LuxeBlogPage';
275

286
const BlogPage = () => {
29-
const [displayItems, setDisplayItems] = useState([]);
30-
const [loading, setLoading] = useState(true);
31-
const [activeFilter, setActiveFilter] = useState('all');
32-
const [searchQuery, setSearchQuery] = useState('');
33-
const [activePost, setActivePost] = useState(null);
34-
35-
useEffect(() => {
36-
const fetchPosts = async () => {
37-
try {
38-
const { processedPosts } = await fetchAllBlogPosts();
39-
40-
const seriesMap = new Map();
41-
const individualPosts = [];
42-
43-
processedPosts.forEach((post) => {
44-
if (post.series) {
45-
if (!seriesMap.has(post.series.slug)) {
46-
seriesMap.set(post.series.slug, {
47-
title: post.series.title,
48-
slug: post.series.slug,
49-
date: post.date,
50-
updated: post.updated,
51-
image: post.series.image,
52-
isSeries: true,
53-
posts: [],
54-
tags: post.tags, // Inherit tags from first post for preview if series
55-
category: post.category,
56-
description: post.series.description || post.description,
57-
});
58-
}
59-
seriesMap.get(post.series.slug).posts.push(post);
60-
} else {
61-
individualPosts.push(post);
62-
}
63-
});
64-
65-
const combinedItems = [
66-
...Array.from(seriesMap.values()),
67-
...individualPosts,
68-
];
69-
combinedItems.sort(
70-
(a, b) =>
71-
new Date(b.updated || b.date) - new Date(a.updated || a.date),
72-
);
7+
const { fezcodexTheme } = useVisualSettings();
738

74-
setDisplayItems(combinedItems);
75-
if (combinedItems.length > 0) setActivePost(combinedItems[0]);
76-
} catch (error) {
77-
console.error('Error fetching blog data:', error);
78-
} finally {
79-
setLoading(false);
80-
}
81-
};
82-
fetchPosts();
83-
}, []);
84-
85-
const filteredItems = displayItems.filter((item) => {
86-
const matchesFilter = () => {
87-
if (activeFilter === 'all') return true;
88-
if (activeFilter === 'series') return item.isSeries;
89-
return item.category === activeFilter && !item.isSeries;
90-
};
91-
const matchesSearch = () => {
92-
if (!searchQuery) return true;
93-
const q = searchQuery.toLowerCase();
94-
return (
95-
item.title?.toLowerCase().includes(q) ||
96-
item.slug?.toLowerCase().includes(q)
97-
);
98-
};
99-
return matchesFilter() && matchesSearch();
100-
});
101-
102-
const isPlaceholder = (post) =>
103-
!post?.image || post.image.includes('placeholder');
104-
105-
if (loading) {
106-
return (
107-
<div className="flex h-screen items-center justify-center bg-[#050505] text-white">
108-
<div className="flex flex-col items-center gap-4">
109-
<div className="h-px w-24 bg-white/10 relative overflow-hidden">
110-
<div className="absolute inset-0 bg-emerald-400 animate-progress origin-left"></div>
111-
</div>
112-
<span className="font-mono text-[10px] text-gray-500 uppercase tracking-[0.3em]">
113-
Accessing_Intel
114-
</span>
115-
</div>
116-
</div>
117-
);
9+
if (fezcodexTheme === 'luxe') {
10+
return <LuxeBlogPage />;
11811
}
11912

120-
return (
121-
<div className="flex min-h-screen bg-[#050505] text-white overflow-hidden relative selection:bg-emerald-500/30">
122-
<Seo
123-
title="Archive | Fezcodex Blog"
124-
description="A curated collection of thoughts, insights, and digital rants."
125-
keywords={['Fezcodex', 'blog', 'developer', 'archive']}
126-
/>
127-
{/* Dynamic Background (Static or Active Post Blur) */}
128-
<div className="absolute inset-0 opacity-20 pointer-events-none z-0">
129-
{activePost &&
130-
(isPlaceholder(activePost) ? (
131-
<GenerativeArt
132-
seed={activePost.title}
133-
className="w-full h-full filter blur-3xl"
134-
/>
135-
) : (
136-
<img
137-
src={activePost.image}
138-
alt="bg"
139-
className="w-full h-full object-cover filter blur-3xl"
140-
/>
141-
))}
142-
</div>
143-
144-
{/* LEFT PANEL: The Index */}
145-
<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">
146-
<header className="mb-16">
147-
<Link
148-
to="/"
149-
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-widest"
150-
>
151-
<ArrowLeft weight="bold" />
152-
<span>Home</span>
153-
</Link>
154-
<h1 className="text-6xl md:text-8xl font-black tracking-tighter text-white mb-4 leading-none">
155-
INTEL
156-
</h1>
157-
<p className="text-gray-400 font-mono text-[10px] uppercase tracking-[0.2em]">
158-
{'//'} DATA_LOGS & ARCHIVED_THOUGHTS
159-
</p>
160-
</header>
161-
162-
{/* Filter Bar & Search */}
163-
<div className="mb-12 border-b border-white/10 pb-8 space-y-6">
164-
<div className="flex flex-wrap items-center gap-2">
165-
{FILTERS.map((f) => (
166-
<button
167-
key={f.id}
168-
onClick={() => setActiveFilter(f.id)}
169-
className={`rounded-sm px-3 py-1 text-[10px] font-bold uppercase tracking-widest transition-all ${
170-
activeFilter === f.id
171-
? 'bg-emerald-500 text-black shadow-lg shadow-emerald-500/20'
172-
: 'bg-white/5 text-gray-500 hover:text-white hover:bg-white/10 border border-white/5'
173-
}`}
174-
>
175-
{f.label}
176-
</button>
177-
))}
178-
</div>
179-
180-
<div className="relative group w-full max-w-md">
181-
<div className="flex items-center gap-2 bg-white/5 border border-white/10 rounded-sm px-3 py-1.5 focus-within:border-emerald-500/50 focus-within:bg-white/10 transition-all">
182-
<MagnifyingGlass size={14} className="text-gray-500 group-focus-within:text-emerald-400" />
183-
<input
184-
type="text"
185-
value={searchQuery}
186-
onChange={(e) => setSearchQuery(e.target.value)}
187-
placeholder="Search Blogposts..."
188-
className="bg-transparent border-none outline-none text-[10px] font-mono uppercase tracking-widest text-white placeholder-gray-600 w-full"
189-
/>
190-
{searchQuery && (
191-
<button
192-
onClick={() => setSearchQuery('')}
193-
className="text-gray-500 hover:text-red-500 transition-colors"
194-
>
195-
<XCircle size={14} weight="fill" />
196-
</button>
197-
)}
198-
</div>
199-
</div>
200-
</div>
201-
202-
<div className="flex flex-col pb-32">
203-
{filteredItems.map((item) => (
204-
<PostItem
205-
key={item.slug}
206-
{...item}
207-
isActive={activePost?.slug === item.slug}
208-
onHover={setActivePost}
209-
/>
210-
))}
211-
{filteredItems.length === 0 && (
212-
<div className="py-12 text-center border border-dashed border-white/10 rounded-lg">
213-
<p className="font-mono text-xs text-gray-500 uppercase tracking-widest">
214-
No_Intel_Found
215-
</p>
216-
</div>
217-
)}
218-
</div>
219-
220-
<div className="mt-auto pt-20 border-t border-white/10 text-gray-600 font-mono text-[10px] uppercase tracking-widest">
221-
Total Stored Entries: {displayItems.length}
222-
</div>
223-
</div>
224-
225-
{/* RIGHT PANEL: The Stage */}
226-
<div className="hidden 4xl:block fixed right-0 top-0 h-screen w-1/2 bg-neutral-900 overflow-hidden border-l border-white/10 z-20">
227-
<AnimatePresence mode="wait">
228-
{activePost && (
229-
<motion.div
230-
key={activePost.slug}
231-
initial={{ opacity: 0 }}
232-
animate={{ opacity: 1 }}
233-
exit={{ opacity: 0 }}
234-
transition={{ duration: 0.4 }}
235-
className="absolute inset-0"
236-
>
237-
{/* Art */}
238-
<div className="absolute inset-0 z-0">
239-
<GenerativeArt
240-
seed={activePost.title}
241-
className="w-full h-full opacity-60"
242-
/>
243-
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-black/40" />
244-
</div>
245-
246-
{/* Details Overlay */}
247-
<div className="absolute bottom-0 left-0 w-full p-16 z-10 flex flex-col gap-8">
248-
<div className="flex items-center gap-6">
249-
<div className="flex items-center gap-2 text-emerald-400 font-mono text-[10px] tracking-widest uppercase">
250-
<Clock size={16} />
251-
<span>
252-
{new Date(
253-
activePost.updated || activePost.date,
254-
).toLocaleDateString('en-GB')}
255-
</span>
256-
</div>
257-
<div className="flex items-center gap-2 text-white font-mono text-[10px] tracking-widest uppercase bg-white/10 px-2 py-1 border border-white/10 rounded-sm">
258-
<Tag size={14} />
259-
<span>{activePost.category || 'Post'}</span>
260-
</div>
261-
</div>
262-
263-
<div className="flex flex-col gap-4">
264-
<h2 className="text-6xl md:text-7xl font-instr-serif text-white uppercase tracking-normal leading-none">
265-
{activePost.title}
266-
</h2>
267-
<p className="text-lg text-gray-300 font-syne leading-relaxed max-w-xl">
268-
{activePost.description ||
269-
'Archived content from the digital vault. Processed and cataloged for immediate access.'}
270-
</p>
271-
</div>
272-
273-
{/* Tags Section */}
274-
{activePost.tags && activePost.tags.length > 0 && (
275-
<div className="flex flex-wrap gap-2 max-w-xl">
276-
{activePost.tags.map((tag) => (
277-
<span
278-
key={tag}
279-
className="inline-flex items-center gap-1 text-[9px] font-mono font-bold uppercase tracking-wider text-gray-400 bg-black/40 px-2 py-1 rounded-sm border border-white/5"
280-
>
281-
<Hash size={10} /> {tag}
282-
</span>
283-
))}
284-
</div>
285-
)}
286-
287-
{activePost.isSeries && (
288-
<div className="mt-4 flex flex-col gap-4">
289-
<span className="font-mono text-[10px] text-emerald-500 font-bold tracking-widest uppercase">
290-
{'//'} SERIES_MANIFEST
291-
</span>
292-
<div className="grid grid-cols-1 gap-2">
293-
{activePost.posts?.slice(0, 3).map((p, i) => (
294-
<div
295-
key={p.slug}
296-
className="flex items-center gap-3 text-gray-500 font-mono text-[10px] uppercase"
297-
>
298-
<span>{String(i + 1).padStart(2, '0')}</span>
299-
<span className="h-px w-4 bg-gray-800" />
300-
<span className="truncate">{p.title}</span>
301-
</div>
302-
))}
303-
</div>
304-
</div>
305-
)}
306-
307-
<motion.div
308-
initial={{ opacity: 0, y: 10 }}
309-
animate={{ opacity: 1, y: 0 }}
310-
transition={{ delay: 0.2 }}
311-
className="mt-8"
312-
>
313-
<Link
314-
to={
315-
activePost.isSeries
316-
? `/blog/series/${activePost.slug}`
317-
: `/blog/${activePost.slug}`
318-
}
319-
className="inline-flex items-center gap-4 text-white border-b-2 border-emerald-500 pb-2 hover:bg-emerald-500 hover:text-black transition-all px-2 py-2"
320-
>
321-
<span className="text-sm font-syne font-normal uppercase tracking-[0.2em]">
322-
Read Post
323-
</span>
324-
<BookOpen weight="bold" size={20} />
325-
</Link>
326-
</motion.div>
327-
</div>
328-
</motion.div>
329-
)}
330-
</AnimatePresence>
331-
</div>
332-
</div>
333-
);
13+
return <BrutalistBlogPage />;
33414
};
33515

336-
export default BlogPage;
16+
export default BlogPage;

0 commit comments

Comments
 (0)