Skip to content

Commit 690039a

Browse files
committed
feat(terracotta): add TerracottaLogsPage
1 parent de96857 commit 690039a

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
FunnelIcon,
6+
XCircleIcon,
7+
InfoIcon,
8+
SquaresFourIcon,
9+
ListIcon,
10+
BookBookmarkIcon,
11+
} from '@phosphor-icons/react';
12+
import { motion, AnimatePresence } from 'framer-motion';
13+
import LogCard from '../../components/LogCard';
14+
import Seo from '../../components/Seo';
15+
import usePersistentState from '../../hooks/usePersistentState';
16+
import colors from '../../config/colors';
17+
import { useAchievements } from '../../context/AchievementContext';
18+
import piml from 'piml';
19+
import GenericModal from '../../components/GenericModal';
20+
import { useSidePanel } from '../../context/SidePanelContext';
21+
import RatingSystemDetail from '../../components/RatingSystemDetail';
22+
23+
const categories = [
24+
'Book', 'Movie', 'Video', 'Game', 'Article', 'Music', 'Series', 'Food', 'Websites', 'Tools', 'Event',
25+
];
26+
27+
const TerracottaLogsPage = () => {
28+
const [logs, setLogs] = useState([]);
29+
const [loading, setLoading] = useState(true);
30+
const [selectedCategories, setSelectedCategories] = useState([]);
31+
const [searchQuery, setSearchQuery] = useState('');
32+
const [filteredLogs, setFilteredLogs] = useState([]);
33+
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false);
34+
const [viewMode, setViewMode] = usePersistentState('fez_logs_view_mode', 'grid');
35+
const { unlockAchievement } = useAchievements();
36+
const { openSidePanel } = useSidePanel();
37+
38+
useEffect(() => {
39+
unlockAchievement('log_diver');
40+
const fetchLogs = async () => {
41+
try {
42+
const fetchPromises = categories.map(async (category) => {
43+
const response = await fetch(
44+
`/logs/${category.toLowerCase()}/${category.toLowerCase()}.piml`,
45+
);
46+
if (!response.ok) return [];
47+
const text = await response.text();
48+
const data = piml.parse(text);
49+
return data.logs || [];
50+
});
51+
52+
const allLogsArrays = await Promise.all(fetchPromises);
53+
const combinedLogs = allLogsArrays.flat();
54+
55+
const logsWithId = combinedLogs
56+
.map((log, index) => ({
57+
...log,
58+
id: `${log.title}-${log.date}-${index}`,
59+
originalIndex: index,
60+
}))
61+
.sort((a, b) => new Date(b.date) - new Date(a.date));
62+
setLogs(logsWithId);
63+
} catch (err) {
64+
console.error('Error fetching logs:', err);
65+
} finally {
66+
setLoading(false);
67+
}
68+
};
69+
fetchLogs();
70+
}, [unlockAchievement]);
71+
72+
useEffect(() => {
73+
const lowerQuery = searchQuery.toLowerCase();
74+
setFilteredLogs(
75+
logs.filter((log) => {
76+
const matchesCategory =
77+
selectedCategories.length === 0 || selectedCategories.includes(log.category);
78+
const matchesSearch =
79+
log.title.toLowerCase().includes(lowerQuery) ||
80+
(log.author && log.author.toLowerCase().includes(lowerQuery)) ||
81+
(log.description && log.description.toLowerCase().includes(lowerQuery));
82+
return matchesCategory && matchesSearch;
83+
}),
84+
);
85+
}, [logs, selectedCategories, searchQuery]);
86+
87+
const toggleCategory = (category) => {
88+
setSelectedCategories((prev) =>
89+
prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category],
90+
);
91+
};
92+
93+
return (
94+
<div className="min-h-screen bg-[#F3ECE0] text-[#1A1613] selection:bg-[#C96442]/25 font-playfairDisplay">
95+
<Seo title="Logs | Fezcodex" description="A collection of logs and writings." />
96+
<div className="mx-auto max-w-5xl px-6 py-24 md:px-12">
97+
<header className="mb-20">
98+
<Link
99+
to="/"
100+
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-[#2E2620]/60 hover:text-[#1A1613] transition-colors uppercase tracking-widest"
101+
>
102+
<ArrowLeftIcon weight="bold" />
103+
<span>Home</span>
104+
</Link>
105+
106+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-8">
107+
<div>
108+
<h1 className="text-6xl md:text-8xl font-playfairDisplay italic tracking-tight text-[#1A1613] mb-4 leading-none">
109+
Logs
110+
</h1>
111+
<p className="text-[#2E2620]/60 font-mono text-sm max-w-sm uppercase tracking-widest">
112+
Total Entries: {logs.length}
113+
</p>
114+
</div>
115+
116+
<div className="flex items-center gap-6">
117+
<div className="flex bg-[#E8DECE]/60 p-1 rounded-sm border border-[#1A161320]">
118+
<button
119+
onClick={() => setViewMode('grid')}
120+
className={`p-2 transition-all ${viewMode === 'grid' ? 'bg-[#1A1613] text-[#F3ECE0]' : 'text-[#2E2620]/60 hover:text-[#1A1613]'}`}
121+
title="Grid View"
122+
>
123+
<SquaresFourIcon size={18} weight={viewMode === 'grid' ? 'fill' : 'regular'} />
124+
</button>
125+
<button
126+
onClick={() => setViewMode('list')}
127+
className={`p-2 transition-all ${viewMode === 'list' ? 'bg-[#1A1613] text-[#F3ECE0]' : 'text-[#2E2620]/60 hover:text-[#1A1613]'}`}
128+
title="List View"
129+
>
130+
<ListIcon size={18} weight={viewMode === 'list' ? 'fill' : 'regular'} />
131+
</button>
132+
</div>
133+
134+
<Link
135+
to="/reading"
136+
className="flex items-center gap-2 px-4 py-2 bg-[#B88532]/15 border border-[#B88532]/60 text-[#B88532] hover:bg-[#B88532] hover:text-[#F3ECE0] transition-all font-mono text-xs uppercase tracking-widest font-bold rounded-sm"
137+
>
138+
<BookBookmarkIcon size={16} weight="bold" />
139+
<span>Reading</span>
140+
</Link>
141+
142+
<button
143+
onClick={() => setIsInfoModalOpen(true)}
144+
className="text-[#2E2620]/60 hover:text-[#9E4A2F] transition-colors font-mono text-xs uppercase tracking-widest flex items-center gap-2"
145+
>
146+
<InfoIcon size={16} />
147+
<span>Rating System</span>
148+
</button>
149+
</div>
150+
</div>
151+
</header>
152+
153+
<div className="sticky top-0 z-30 bg-[#F3ECE0]/95 backdrop-blur-sm border-b border-[#1A161320] pb-6 pt-2 mb-12">
154+
<div className="mb-6">
155+
<input
156+
type="text"
157+
placeholder="Search logs..."
158+
value={searchQuery}
159+
onChange={(e) => setSearchQuery(e.target.value)}
160+
className="w-full bg-transparent border-b border-[#1A161320] text-xl md:text-2xl text-[#1A1613] placeholder-[#2E2620]/30 focus:border-[#C96442] focus:outline-none py-2 transition-colors font-playfairDisplay italic"
161+
/>
162+
</div>
163+
164+
<div className="flex flex-wrap items-center gap-3">
165+
<div className="flex items-center gap-2 mr-2 text-[#2E2620]/50 font-mono text-xs uppercase tracking-widest">
166+
<FunnelIcon size={14} weight="fill" />
167+
<span>Filter By</span>
168+
</div>
169+
170+
{categories.map((category) => {
171+
const isSelected = selectedCategories.includes(category);
172+
const color = colors[category.toLowerCase()] || colors.primary[400];
173+
174+
return (
175+
<button
176+
key={category}
177+
onClick={() => toggleCategory(category)}
178+
className={`px-3 py-1 rounded-sm text-xs font-bold uppercase tracking-widest border transition-all duration-200 ${
179+
isSelected
180+
? 'bg-[#1A1613] text-[#F3ECE0] border-[#1A1613]'
181+
: 'bg-transparent text-[#2E2620]/60 border-[#1A161320] hover:border-[#1A1613] hover:text-[#1A1613]'
182+
}`}
183+
style={
184+
isSelected
185+
? { borderColor: color, color: color, backgroundColor: `${color}15` }
186+
: {}
187+
}
188+
>
189+
{category}
190+
</button>
191+
);
192+
})}
193+
{(selectedCategories.length > 0 || searchQuery) && (
194+
<button
195+
onClick={() => {
196+
setSelectedCategories([]);
197+
setSearchQuery('');
198+
}}
199+
className="ml-auto text-[#9E4A2F] hover:text-[#C96442] transition-colors"
200+
>
201+
<XCircleIcon size={20} weight="fill" />
202+
</button>
203+
)}
204+
</div>
205+
</div>
206+
207+
{loading ? (
208+
<div
209+
className={
210+
viewMode === 'grid'
211+
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-pulse'
212+
: 'flex flex-col gap-4 animate-pulse'
213+
}
214+
>
215+
{[...Array(8)].map((_, i) => (
216+
<div
217+
key={i}
218+
className={viewMode === 'grid' ? 'h-64 w-full bg-[#E8DECE]/60 rounded-sm' : 'h-20 w-full bg-[#E8DECE]/60 rounded-sm'}
219+
/>
220+
))}
221+
</div>
222+
) : (
223+
<div className="pb-32">
224+
<AnimatePresence mode="popLayout">
225+
<motion.div
226+
layout
227+
className={
228+
viewMode === 'grid'
229+
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6'
230+
: 'flex flex-col'
231+
}
232+
>
233+
{filteredLogs.map((log, ndx) => (
234+
<motion.div
235+
layout
236+
key={log.id}
237+
initial={{ opacity: 0, scale: 0.95 }}
238+
animate={{ opacity: 1, scale: 1 }}
239+
exit={{ opacity: 0, scale: 0.95 }}
240+
transition={{ duration: 0.2 }}
241+
>
242+
<LogCard log={log} index={ndx} totalLogs={logs.length} viewMode={viewMode} />
243+
</motion.div>
244+
))}
245+
</motion.div>
246+
</AnimatePresence>
247+
248+
{filteredLogs.length === 0 && (
249+
<div className="py-20 text-center font-mono text-[#2E2620]/50 uppercase tracking-widest">
250+
No logs found.
251+
</div>
252+
)}
253+
</div>
254+
)}
255+
</div>
256+
257+
<GenericModal
258+
isOpen={isInfoModalOpen}
259+
onClose={() => setIsInfoModalOpen(false)}
260+
title="Rating System"
261+
>
262+
<div className="font-mono text-sm space-y-6 text-[#2E2620]">
263+
<p className="tracking-wide leading-relaxed">
264+
This archive serves as a repository for tracking media consumption and subjective evaluation.
265+
</p>
266+
267+
<div className="border border-[#1A161320] p-4 bg-[#E8DECE]/40">
268+
<h3 className="text-[#9E4A2F] font-bold mb-4 uppercase tracking-widest border-b border-[#9E4A2F]/30 pb-2">
269+
Rating Scale
270+
</h3>
271+
<ul className="space-y-3">
272+
<li className="flex justify-between"><span>5 Stars</span><span className="text-[#2E2620]/60 uppercase">Masterpiece</span></li>
273+
<li className="flex justify-between"><span>4 Stars</span><span className="text-[#2E2620]/60 uppercase">Highly Recommended</span></li>
274+
<li className="flex justify-between"><span>3 Stars</span><span className="text-[#2E2620]/60 uppercase">Good / Worthwhile</span></li>
275+
<li className="flex justify-between"><span>2 Stars</span><span className="text-[#2E2620]/60 uppercase">Flawed / Average</span></li>
276+
<li className="flex justify-between"><span>1 Star</span><span className="text-[#2E2620]/60 uppercase">Avoid</span></li>
277+
</ul>
278+
</div>
279+
280+
<button
281+
onClick={() => {
282+
setIsInfoModalOpen(false);
283+
unlockAchievement('east_side');
284+
openSidePanel('Rating System Details', <RatingSystemDetail />, 600);
285+
}}
286+
className="w-full py-3 border border-[#C96442] text-[#9E4A2F] hover:bg-[#C96442] hover:text-[#F3ECE0] transition-all uppercase tracking-widest font-bold"
287+
>
288+
Read full guide
289+
</button>
290+
</div>
291+
</GenericModal>
292+
</div>
293+
);
294+
};
295+
296+
export default TerracottaLogsPage;

0 commit comments

Comments
 (0)