Skip to content

Commit 3155842

Browse files
committed
feat(terracotta): add TerracottaAppsPage
1 parent 17d057e commit 3155842

1 file changed

Lines changed: 365 additions & 0 deletions

File tree

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import React, { useState, useEffect, useMemo } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
MagnifyingGlassIcon,
6+
CaretDownIcon,
7+
XIcon,
8+
FingerprintIcon,
9+
CubeIcon,
10+
ShieldCheckIcon,
11+
ClockIcon,
12+
ArrowUpRightIcon,
13+
AtomIcon,
14+
} from '@phosphor-icons/react';
15+
import { motion, AnimatePresence } from 'framer-motion';
16+
import Seo from '../../components/Seo';
17+
import { appIcons } from '../../utils/appIcons';
18+
import usePersistentState from '../../hooks/usePersistentState';
19+
import { KEY_APPS_COLLAPSED_CATEGORIES } from '../../utils/LocalStorageManager';
20+
import ArcaneSigil from '../../components/ArcaneSigil';
21+
22+
const SectionBackground = ({ seed }) => (
23+
<>
24+
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-[0.05]">
25+
<svg width="100%" height="100%" className="absolute inset-0">
26+
<pattern id={`pattern-${seed}`} x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
27+
<path d="M 10 10 L 90 10 L 90 90 L 10 90 Z" fill="none" stroke="currentColor" strokeWidth="0.5" />
28+
<circle cx="50" cy="50" r="1" fill="currentColor" />
29+
</pattern>
30+
<rect width="100%" height="100%" fill={`url(#pattern-${seed})`} />
31+
</svg>
32+
</div>
33+
<div className="absolute right-0 top-0 w-96 h-96 -mr-32 -mt-32 pointer-events-none opacity-10">
34+
<ArcaneSigil seed={seed} color="currentColor" className="w-full h-full" />
35+
</div>
36+
</>
37+
);
38+
39+
const AppModule = ({ app, index, categoryName }) => {
40+
const AppIcon = appIcons[app.icon] || appIcons[`${app.icon}Icon`] || CubeIcon;
41+
42+
return (
43+
<motion.div
44+
initial={{ opacity: 0, y: 10 }}
45+
animate={{ opacity: 1, y: 0 }}
46+
whileHover={{ x: -4, y: -4 }}
47+
transition={{ delay: index * 0.02, type: 'spring', stiffness: 400, damping: 15 }}
48+
className="group relative"
49+
>
50+
<Link
51+
to={app.to}
52+
className="block relative aspect-square md:aspect-auto md:h-64 bg-[#F3ECE0] border border-[#1A161320] overflow-hidden hover:bg-[#1A1613] transition-all duration-300 shadow-[0_20px_40px_-25px_#1A161330]"
53+
>
54+
<div className="absolute inset-0 flex items-center justify-center opacity-[0.06] group-hover:opacity-[0.12] transition-opacity duration-700 overflow-hidden pointer-events-none">
55+
<div className="w-full h-full border-[0.5px] border-current opacity-30 rotate-45 scale-150" />
56+
</div>
57+
58+
<div className="relative p-3 border-b border-[#1A161320] flex justify-between items-center bg-[#E8DECE]/40 group-hover:border-[#F3ECE0]/10 group-hover:bg-[#F3ECE0]/5 transition-colors">
59+
<span className="font-mono text-[9px] uppercase tracking-widest text-[#2E2620]/50 group-hover:text-[#F3ECE0]/50 transition-colors">
60+
{categoryName}
61+
</span>
62+
<div className="flex gap-1">
63+
<div className="w-1 h-1 bg-[#2E2620]/30 rounded-full group-hover:bg-[#F3ECE0]/30" />
64+
</div>
65+
</div>
66+
67+
<div className="relative p-5 flex flex-col h-[calc(100%-40px)] justify-between z-10">
68+
<div className="flex items-start justify-between">
69+
<div className="p-2.5 bg-[#E8DECE]/60 border border-[#1A161320] text-[#1A1613] group-hover:bg-[#F3ECE0] group-hover:text-[#1A1613] group-hover:border-[#F3ECE0] transition-all duration-500">
70+
<AppIcon size={24} weight="light" />
71+
</div>
72+
<ArrowUpRightIcon size={16} className="text-[#2E2620]/30 group-hover:text-[#F3ECE0]/40 transition-colors" />
73+
</div>
74+
75+
<div>
76+
<h3 className="text-2xl font-playfairDisplay italic leading-tight mb-2 text-[#1A1613] group-hover:text-[#F3ECE0] transition-colors">
77+
{app.title}
78+
</h3>
79+
<p className="text-xs font-mono text-[#2E2620]/60 line-clamp-2 leading-relaxed group-hover:text-[#F3ECE0]/60 transition-colors">
80+
{app.description}
81+
</p>
82+
</div>
83+
84+
<div className="flex items-center gap-3 mt-4 opacity-0 group-hover:opacity-100 transition-all">
85+
<div className="h-[1px] flex-1 bg-[#F3ECE0]/20" />
86+
<span className="font-mono text-[8px] uppercase tracking-[0.3em] text-[#F3ECE0]/60">Initialize</span>
87+
</div>
88+
</div>
89+
</Link>
90+
</motion.div>
91+
);
92+
};
93+
94+
const TerracottaAppsPage = () => {
95+
const [groupedApps, setGroupedApps] = useState({});
96+
const [isLoading, setIsLoading] = useState(true);
97+
const [searchQuery, setSearchQuery] = useState('');
98+
const [collapsedCategories, setCollapsedCategories] = usePersistentState(
99+
KEY_APPS_COLLAPSED_CATEGORIES,
100+
{},
101+
);
102+
103+
useEffect(() => {
104+
setIsLoading(true);
105+
fetch('/apps/apps.json')
106+
.then((res) => res.json())
107+
.then((data) => {
108+
setGroupedApps(data);
109+
setCollapsedCategories((prev) => {
110+
const next = { ...prev };
111+
Object.keys(data).forEach((k) => {
112+
if (next[k] === undefined) next[k] = false;
113+
});
114+
return next;
115+
});
116+
})
117+
.catch((err) => console.error('Failed to fetch apps:', err))
118+
.finally(() => setIsLoading(false));
119+
}, [setCollapsedCategories]);
120+
121+
const appsFlat = useMemo(() => {
122+
const list = [];
123+
Object.keys(groupedApps).forEach((k) => {
124+
groupedApps[k].apps.forEach((app) => {
125+
list.push({ ...app, category: k });
126+
});
127+
});
128+
return list;
129+
}, [groupedApps]);
130+
131+
const filteredData = useMemo(() => {
132+
if (!searchQuery) return groupedApps;
133+
const query = searchQuery.toLowerCase();
134+
const result = {};
135+
Object.keys(groupedApps).forEach((k) => {
136+
const filtered = groupedApps[k].apps.filter(
137+
(app) =>
138+
app.title.toLowerCase().includes(query) ||
139+
app.description.toLowerCase().includes(query),
140+
);
141+
if (filtered.length > 0) {
142+
result[k] = { ...groupedApps[k], apps: filtered };
143+
}
144+
});
145+
return result;
146+
}, [groupedApps, searchQuery]);
147+
148+
const toggleCategory = (key) => {
149+
setCollapsedCategories((prev) => ({ ...prev, [key]: !prev[key] }));
150+
};
151+
152+
return (
153+
<div className="min-h-screen bg-[#F3ECE0] text-[#1A1613] selection:bg-[#C96442]/25 font-playfairDisplay overflow-x-hidden">
154+
<Seo title="Archive // Fezcodex" description="A collection of digital tools and artifacts." />
155+
156+
<div className="fixed inset-0 z-0 pointer-events-none opacity-20">
157+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1A161308_1px,transparent_1px),linear-gradient(to_bottom,#1A161308_1px,transparent_1px)] bg-[size:40px_40px]" />
158+
</div>
159+
160+
<div className="relative z-10 max-w-[1800px] mx-auto p-6 md:p-12">
161+
<header className="grid grid-cols-1 lg:grid-cols-12 gap-12 mb-24 pb-12 border-b border-[#1A161320]">
162+
<div className="lg:col-span-8">
163+
<Link
164+
to="/"
165+
className="inline-flex items-center gap-2 group text-[10px] uppercase tracking-[0.4em] text-[#2E2620]/50 hover:text-[#1A1613] transition-colors mb-12"
166+
>
167+
<ArrowLeftIcon size={14} className="group-hover:-translate-x-1 transition-transform" />
168+
<span>Return to Home</span>
169+
</Link>
170+
171+
<h1 className="text-6xl md:text-9xl font-playfairDisplay italic tracking-tight leading-[0.85] mb-8 text-[#1A1613]">
172+
Application
173+
<br />
174+
Archive
175+
</h1>
176+
177+
<p className="text-[#2E2620] max-w-xl text-lg leading-relaxed">
178+
An exhaustive collection of experimental software, digital utilities, and interactive artifacts.
179+
</p>
180+
</div>
181+
182+
<div className="lg:col-span-4 flex flex-col justify-end space-y-8">
183+
<div className="bg-[#E8DECE]/60 border border-[#1A161320] p-5 font-mono text-[10px] uppercase tracking-[0.2em] space-y-2">
184+
<div className="flex justify-between border-b border-[#1A161320] pb-2">
185+
<span className="text-[#2E2620]/60">Total Items</span>
186+
<span className="text-[#1A1613]">{appsFlat.length}</span>
187+
</div>
188+
<div className="flex justify-between">
189+
<span className="text-[#2E2620]/60">Status</span>
190+
<div className="flex items-center gap-2">
191+
<span className="w-2 h-2 bg-[#6B8E23] rounded-full animate-pulse" />
192+
<span className="text-[#6B8E23]">Active</span>
193+
</div>
194+
</div>
195+
</div>
196+
197+
<div className="relative">
198+
<input
199+
type="text"
200+
placeholder="Filter by name..."
201+
value={searchQuery}
202+
onChange={(e) => setSearchQuery(e.target.value)}
203+
className="w-full bg-transparent border-b-2 border-[#1A161320] py-6 px-2 focus:border-[#C96442] focus:outline-none transition-all font-playfairDisplay italic text-3xl placeholder-[#2E2620]/15 text-[#1A1613]"
204+
/>
205+
<MagnifyingGlassIcon size={32} className="absolute right-4 top-1/2 -translate-y-1/2 text-[#2E2620]/30" />
206+
</div>
207+
</div>
208+
</header>
209+
210+
<div className="grid grid-cols-1 xl:grid-cols-12 gap-12">
211+
<aside className="xl:col-span-3 hidden xl:block space-y-12">
212+
<section className="space-y-6">
213+
<div className="flex items-center gap-3">
214+
<ClockIcon size={20} className="text-[#2E2620]/60" />
215+
<h2 className="font-mono text-[10px] uppercase tracking-[0.4em] text-[#2E2620]/80">
216+
Recently Added
217+
</h2>
218+
</div>
219+
<div className="space-y-4">
220+
{appsFlat
221+
.filter((a) => a.created_at)
222+
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
223+
.slice(0, 4)
224+
.map((app) => (
225+
<Link
226+
key={app.slug}
227+
to={app.to}
228+
className="group block p-4 bg-[#E8DECE]/40 border border-[#1A161320] hover:border-[#1A1613] transition-all"
229+
>
230+
<span className="block text-sm font-playfairDisplay italic mb-1 group-hover:text-[#1A1613] text-[#2E2620] transition-colors">
231+
{app.title}
232+
</span>
233+
<span className="block text-[9px] font-mono text-[#2E2620]/50 uppercase tracking-widest">
234+
{app.category}
235+
</span>
236+
</Link>
237+
))}
238+
</div>
239+
</section>
240+
241+
<div className="p-8 border border-[#1A161320] bg-[#E8DECE]/30 relative overflow-hidden group">
242+
<div className="flex items-center gap-2 mb-6 font-mono text-[10px] uppercase tracking-widest text-[#2E2620]/60">
243+
<AtomIcon /> System Integrity
244+
</div>
245+
<div className="w-full h-32 flex items-center justify-center opacity-30 group-hover:opacity-60 transition-opacity">
246+
<ArcaneSigil seed="SidebarVisualizer" color="currentColor" className="w-full h-full" />
247+
</div>
248+
</div>
249+
</aside>
250+
251+
<main className="xl:col-span-9 space-y-12">
252+
{isLoading ? (
253+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
254+
{[...Array(9)].map((_, i) => (
255+
<div key={i} className="h-64 bg-[#E8DECE]/60 animate-pulse border border-[#1A161320]" />
256+
))}
257+
</div>
258+
) : (
259+
Object.keys(filteredData)
260+
.sort((a, b) => filteredData[a].order - filteredData[b].order)
261+
.map((categoryKey) => {
262+
const category = filteredData[categoryKey];
263+
const isCollapsed = collapsedCategories[categoryKey];
264+
265+
return (
266+
<section key={categoryKey} className="relative border border-[#1A161320] bg-[#F3ECE0]/50 overflow-hidden">
267+
<SectionBackground seed={categoryKey} />
268+
269+
<button
270+
onClick={() => toggleCategory(categoryKey)}
271+
className="relative w-full flex items-center justify-between p-6 md:p-10 hover:bg-[#E8DECE]/40 transition-colors group z-10"
272+
>
273+
<div className="flex items-center gap-6">
274+
<div className="text-4xl md:text-6xl font-playfairDisplay italic group-hover:translate-x-2 transition-transform text-[#1A1613]">
275+
{category.name}
276+
</div>
277+
<div className="hidden md:block h-[1px] w-32 bg-[#1A161320] group-hover:w-40 transition-all" />
278+
<span className="font-mono text-[10px] text-[#2E2620]/40 uppercase tracking-[0.3em] group-hover:text-[#2E2620]/60">
279+
{category.apps.length} Modules
280+
</span>
281+
</div>
282+
<div className={`transition-transform duration-500 ${isCollapsed ? '' : 'rotate-180'}`}>
283+
<CaretDownIcon size={24} weight="thin" />
284+
</div>
285+
</button>
286+
287+
<AnimatePresence initial={false}>
288+
{!isCollapsed && (
289+
<motion.div
290+
initial={{ height: 0, opacity: 0 }}
291+
animate={{ height: 'auto', opacity: 1 }}
292+
exit={{ height: 0, opacity: 0 }}
293+
transition={{ duration: 0.5, ease: [0.19, 1, 0.22, 1] }}
294+
className="relative overflow-hidden z-10"
295+
>
296+
<div className="p-6 md:p-10 pt-0 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
297+
{category.apps.map((app, i) => (
298+
<AppModule key={app.slug} app={app} index={i} categoryName={category.name} />
299+
))}
300+
</div>
301+
</motion.div>
302+
)}
303+
</AnimatePresence>
304+
</section>
305+
);
306+
})
307+
)}
308+
309+
{!isLoading && Object.keys(filteredData).length === 0 && (
310+
<div className="py-32 text-center">
311+
<XIcon size={64} weight="thin" className="mx-auto mb-8 text-[#1A161320]" />
312+
<h2 className="text-4xl font-playfairDisplay italic mb-4 text-[#1A1613]">No Entries Found</h2>
313+
<p className="text-[#2E2620]/50 uppercase tracking-[0.2em] text-[10px]">Modify filter and retry</p>
314+
</div>
315+
)}
316+
</main>
317+
</div>
318+
319+
<footer className="mt-48 pt-12 border-t border-[#1A161320] grid grid-cols-1 md:grid-cols-3 gap-12 opacity-40 hover:opacity-100 transition-all duration-700">
320+
<div className="space-y-4">
321+
<div className="flex items-center gap-3 text-[#1A1613]">
322+
<ShieldCheckIcon size={24} />
323+
<span className="font-mono text-[10px] uppercase tracking-[0.4em]">Secure Archive</span>
324+
</div>
325+
<p className="text-[9px] uppercase tracking-widest leading-relaxed text-[#2E2620]">
326+
All artifacts are property of the Fezcodex Collective.
327+
</p>
328+
</div>
329+
330+
<div className="flex flex-col justify-center items-center gap-3">
331+
<div className="flex gap-1">
332+
{[...Array(12)].map((_, i) => (
333+
<div key={i} className={`w-1 h-3 ${i < 9 ? 'bg-[#1A1613]' : 'bg-[#1A161320]'}`} />
334+
))}
335+
</div>
336+
<span className="font-mono text-[8px] uppercase tracking-[0.3em] text-[#1A1613]">
337+
Protocol Synchronized
338+
</span>
339+
</div>
340+
341+
<div className="text-right flex flex-col items-end justify-between">
342+
<div className="flex items-center gap-4">
343+
<div className="flex flex-col items-end">
344+
<span className="text-sm font-playfairDisplay italic leading-none mb-1 text-[#1A1613]">
345+
Ahmed Samil Bulbul
346+
</span>
347+
<span className="font-mono text-[8px] text-[#2E2620]/50 tracking-[0.2em] uppercase">
348+
System Architect
349+
</span>
350+
</div>
351+
<div className="w-12 h-12 bg-[#E8DECE]/60 border border-[#1A161320] flex items-center justify-center">
352+
<FingerprintIcon size={24} weight="thin" />
353+
</div>
354+
</div>
355+
<p className="text-[8px] tracking-[0.5em] uppercase mt-8 text-[#1A1613]">
356+
FCX_OS // Archive v0.9.6
357+
</p>
358+
</div>
359+
</footer>
360+
</div>
361+
</div>
362+
);
363+
};
364+
365+
export default TerracottaAppsPage;

0 commit comments

Comments
 (0)