|
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'; |
17 | 5 |
|
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(); |
28 | 8 |
|
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 | + } |
42 | 12 |
|
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 | +}; |
261 | 15 |
|
262 | 16 | export default AppPage; |
0 commit comments