Skip to content

Commit aafa039

Browse files
committed
feat: new apps pages
1 parent 6a76f74 commit aafa039

File tree

3 files changed

+443
-257
lines changed

3 files changed

+443
-257
lines changed

src/pages/AppPage.jsx

Lines changed: 11 additions & 257 deletions
Original file line numberDiff line numberDiff line change
@@ -1,262 +1,16 @@
1-
import React, { useState, useEffect } from 'react';
2-
import { Link } from 'react-router-dom';
3-
import {
4-
ArrowLeft,
5-
Funnel,
6-
MagnifyingGlass,
7-
CaretRight,
8-
CaretDown,
9-
} from '@phosphor-icons/react';
10-
import { motion, AnimatePresence } from 'framer-motion';
11-
import AppCard from '../components/AppCard';
12-
import Seo from '../components/Seo';
13-
import { appIcons } from '../utils/appIcons';
14-
import CustomDropdown from '../components/CustomDropdown';
15-
import usePersistentState from '../hooks/usePersistentState';
16-
import { KEY_APPS_COLLAPSED_CATEGORIES } from '../utils/LocalStorageManager';
1+
import React from 'react';
2+
import { useVisualSettings } from '../context/VisualSettingsContext';
3+
import BrutalistAppsPage from './brutalist-views/BrutalistAppsPage';
4+
import LuxeAppsPage from './luxe-views/LuxeAppsPage';
175

18-
function AppPage() {
19-
const [groupedApps, setGroupedApps] = useState({});
20-
const [isLoading, setIsLoading] = useState(true);
21-
const [totalAppsCount, setTotalAppsCount] = useState(0);
22-
const [sortOption, setSortOption] = useState('default');
23-
const [searchQuery, setSearchQuery] = useState('');
24-
const [collapsedCategories, setCollapsedCategories] = usePersistentState(
25-
KEY_APPS_COLLAPSED_CATEGORIES,
26-
{},
27-
);
6+
const AppPage = () => {
7+
const { fezcodexTheme } = useVisualSettings();
288

29-
useEffect(() => {
30-
setIsLoading(true);
31-
fetch('/apps/apps.json')
32-
.then((response) => response.json())
33-
.then((data) => {
34-
setGroupedApps(data);
35-
let count = 0;
36-
for (const categoryKey in data) {
37-
if (categoryKey !== 'Bests') {
38-
count += data[categoryKey].apps.length;
39-
}
40-
}
41-
setTotalAppsCount(count);
9+
if (fezcodexTheme === 'luxe') {
10+
return <LuxeAppsPage />;
11+
}
4212

43-
setCollapsedCategories((prevState) => {
44-
const newState = { ...prevState };
45-
Object.keys(data).forEach((key) => {
46-
if (newState[key] === undefined) {
47-
newState[key] = false;
48-
}
49-
});
50-
return newState;
51-
});
52-
})
53-
.catch((error) => console.error('Error fetching apps:', error))
54-
.finally(() => {
55-
setIsLoading(false);
56-
});
57-
}, [setCollapsedCategories]);
58-
59-
const toggleCategoryCollapse = (categoryKey) => {
60-
setCollapsedCategories((prevState) => ({
61-
...prevState,
62-
[categoryKey]: !prevState[categoryKey],
63-
}));
64-
};
65-
66-
const sortApps = (apps) => {
67-
return [...apps].sort((a, b) => {
68-
if (searchQuery) {
69-
const query = searchQuery.toLowerCase();
70-
const matchA =
71-
a.title.toLowerCase().includes(query) ||
72-
a.description.toLowerCase().includes(query);
73-
const matchB =
74-
b.title.toLowerCase().includes(query) ||
75-
b.description.toLowerCase().includes(query);
76-
if (matchA && !matchB) return -1;
77-
if (!matchA && matchB) return 1;
78-
}
79-
if (sortOption === 'default') return 0;
80-
const dateA = a.created_at ? new Date(a.created_at).getTime() : 0;
81-
const dateB = b.created_at ? new Date(b.created_at).getTime() : 0;
82-
return sortOption === 'created_desc' ? dateB - dateA : dateA - dateB;
83-
});
84-
};
85-
86-
const filterApps = (apps) => {
87-
if (!searchQuery) return apps;
88-
const query = searchQuery.toLowerCase();
89-
return apps.filter(
90-
(app) =>
91-
app.title.toLowerCase().includes(query) ||
92-
app.description.toLowerCase().includes(query),
93-
);
94-
};
95-
96-
const variants = {
97-
open: { opacity: 1, height: 'auto', marginBottom: 40 },
98-
collapsed: { opacity: 0, height: 0, marginBottom: 0 },
99-
};
100-
101-
return (
102-
<div className="min-h-screen bg-[#050505] text-white selection:bg-emerald-500/30">
103-
<Seo
104-
title="Apps | Fezcodex"
105-
description="A collection of tools, games, and utilities created within Fezcodex."
106-
keywords={['Fezcodex', 'apps', 'utilities', 'tools', 'react']}
107-
/>
108-
<div className="mx-auto max-w-7xl px-6 py-24 md:px-12">
109-
{/* Header Section */}
110-
<header className="mb-20">
111-
<Link
112-
to="/"
113-
className="mb-8 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-widest"
114-
>
115-
<ArrowLeft weight="bold" />
116-
<span>Home</span>
117-
</Link>
118-
119-
<div className="flex flex-col md:flex-row md:items-end justify-between gap-8">
120-
<div>
121-
<h1 className="text-6xl md:text-8xl font-black tracking-tighter text-white mb-4 leading-none uppercase">
122-
Apps
123-
</h1>
124-
<p className="text-gray-400 font-mono text-sm max-w-sm uppercase tracking-widest">
125-
Total Apps: {totalAppsCount}
126-
</p>
127-
</div>
128-
</div>
129-
</header>
130-
131-
{/* Controls: Search & Sort */}
132-
<div className="sticky top-0 z-30 bg-[#050505]/95 backdrop-blur-sm border-b border-white/10 pb-6 pt-2 mb-12">
133-
<div className="flex flex-col md:flex-row gap-6 items-center">
134-
<div className="relative flex-1 group">
135-
<MagnifyingGlass
136-
className="absolute left-0 top-1/2 -translate-y-1/2 text-gray-600 group-focus-within:text-emerald-500 transition-colors"
137-
size={20}
138-
/>
139-
<input
140-
type="text"
141-
placeholder="Search apps..."
142-
value={searchQuery}
143-
onChange={(e) => setSearchQuery(e.target.value)}
144-
className="w-full bg-transparent border-b border-gray-800 pl-8 text-xl md:text-2xl font-light text-white placeholder-gray-700 focus:border-emerald-500 focus:outline-none py-2 transition-colors font-mono"
145-
/>
146-
</div>
147-
<div className="w-full md:w-64">
148-
<CustomDropdown
149-
options={[
150-
{ label: 'Default Order', value: 'default' },
151-
{ label: 'Newest First', value: 'created_desc' },
152-
{ label: 'Oldest First', value: 'created_asc' },
153-
]}
154-
value={sortOption}
155-
onChange={setSortOption}
156-
icon={Funnel}
157-
label="Sort"
158-
variant="brutalist"
159-
/>
160-
</div>
161-
</div>
162-
</div>
163-
164-
{isLoading ? (
165-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
166-
{[...Array(6)].map((_, i) => (
167-
<div
168-
key={i}
169-
className="h-64 w-full bg-white/5 border border-white/10 rounded-sm animate-pulse"
170-
/>
171-
))}
172-
</div>
173-
) : (
174-
<div className="space-y-16">
175-
{Object.keys(groupedApps)
176-
.sort((a, b) => groupedApps[a].order - groupedApps[b].order)
177-
.map((categoryKey) => {
178-
const category = groupedApps[categoryKey];
179-
const CategoryIcon = appIcons[category.icon] || appIcons[`${category.icon}Icon`];
180-
const filteredApps = filterApps(category.apps);
181-
const sortedApps = sortApps(filteredApps);
182-
const isCollapsed = collapsedCategories[categoryKey];
183-
184-
if (sortedApps.length === 0) return null;
185-
186-
return (
187-
<div key={categoryKey} className="relative">
188-
<button
189-
className="w-full flex items-center justify-between py-4 border-b border-white/10 group mb-8"
190-
onClick={() => toggleCategoryCollapse(categoryKey)}
191-
>
192-
<div className="flex items-center gap-4">
193-
<div
194-
className={`p-2 transition-colors ${isCollapsed ? 'text-gray-600' : 'text-emerald-500'}`}
195-
>
196-
{CategoryIcon && (
197-
<CategoryIcon
198-
size={24}
199-
weight={isCollapsed ? 'regular' : 'fill'}
200-
/>
201-
)}
202-
</div>
203-
<div className="text-left">
204-
<h2 className="text-2xl font-bold font-sans uppercase tracking-tight text-gray-200 group-hover:text-white transition-colors">
205-
{category.name}
206-
<span className="ml-4 font-mono text-xs font-normal text-gray-500">
207-
[{sortedApps.length}]
208-
</span>
209-
</h2>
210-
</div>
211-
</div>
212-
<div className="text-gray-600 group-hover:text-white transition-colors">
213-
{isCollapsed ? (
214-
<CaretRight size={20} weight="bold" />
215-
) : (
216-
<CaretDown size={20} weight="bold" />
217-
)}
218-
</div>
219-
</button>
220-
221-
<AnimatePresence initial={false}>
222-
{!isCollapsed && (
223-
<motion.div
224-
key="content"
225-
initial="collapsed"
226-
animate="open"
227-
exit="collapsed"
228-
variants={variants}
229-
transition={{
230-
duration: 0.4,
231-
ease: [0.23, 1, 0.32, 1],
232-
}}
233-
className="overflow-hidden"
234-
>
235-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
236-
{sortedApps.map((app, index) => (
237-
<AppCard key={app.slug || index} app={app} />
238-
))}
239-
</div>
240-
</motion.div>
241-
)}
242-
</AnimatePresence>
243-
</div>
244-
);
245-
})}
246-
247-
{searchQuery &&
248-
Object.keys(groupedApps).every(
249-
(key) => filterApps(groupedApps[key].apps).length === 0,
250-
) && (
251-
<div className="py-32 text-center font-mono text-gray-600 uppercase tracking-widest border border-dashed border-white/10">
252-
No apps matching "{searchQuery}" found in archive.
253-
</div>
254-
)}
255-
</div>
256-
)}
257-
</div>
258-
</div>
259-
);
260-
}
13+
return <BrutalistAppsPage />;
14+
};
26115

26216
export default AppPage;

0 commit comments

Comments
 (0)