Skip to content

Commit 71de06d

Browse files
committed
feat: Pinned Apps page
1 parent 785d7ed commit 71de06d

File tree

4 files changed

+216
-1
lines changed

4 files changed

+216
-1
lines changed

public/apps/apps.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"title": "FezType (Typing Speed Tester)",
1212
"description": "Test and improve your typing speed.",
1313
"icon": "KeyboardIcon",
14+
"pinned_order": 4,
1415
"created_at": "2025-11-10T14:00:00+03:00"
1516
},
1617
{
@@ -19,6 +20,7 @@
1920
"title": "Mastermind",
2021
"description": "Play the classic code-breaking game of Mastermind (Bulls and Cows).",
2122
"icon": "PuzzlePieceIcon",
23+
"pinned_order": 5,
2224
"created_at": "2025-11-23T18:28:07+03:00"
2325
},
2426
{
@@ -27,6 +29,7 @@
2729
"title": "Nonogram",
2830
"description": "Solve picture logic puzzles by filling cells according to numerical clues.",
2931
"icon": "GridFourIcon",
32+
"pinned_order": 6,
3033
"created_at": "2025-11-23T20:43:44+03:00"
3134
},
3235
{
@@ -132,6 +135,7 @@
132135
"title": "TCG Card Generator",
133136
"description": "Create your own custom TCG cards with this generator.",
134137
"icon": "CardsThreeIcon",
138+
"pinned_order": 2,
135139
"created_at": "2025-11-26T12:00:00+03:00"
136140
},
137141
{
@@ -140,6 +144,7 @@
140144
"title": "Football Emblem Creator",
141145
"description": "Create your own custom football team emblem.",
142146
"icon": "TrophyIcon",
147+
"pinned_order": 3,
143148
"created_at": "2025-11-25T18:00:00+03:00"
144149
},
145150
{
@@ -148,6 +153,7 @@
148153
"title": "Tournament Bracket",
149154
"description": "Create and manage tournament brackets.",
150155
"icon": "ListNumbersIcon",
156+
"pinned_order": 7,
151157
"created_at": "2025-11-07T14:22:02+03:00"
152158
},
153159
{
@@ -172,6 +178,7 @@
172178
"title": "Whiteboard",
173179
"description": "A simple digital whiteboard for sketching and doodling.",
174180
"icon": "PencilSimpleIcon",
181+
"pinned_order": 8,
175182
"created_at": "2025-11-25T17:00:00+03:00"
176183
},
177184
{
@@ -252,6 +259,7 @@
252259
"title": "Color Palette Generator",
253260
"description": "Generate random color palettes.",
254261
"icon": "PaletteIcon",
262+
"pinned_order": 9,
255263
"created_at": "2025-11-07T19:33:40+03:00"
256264
},
257265
{
@@ -324,6 +332,7 @@
324332
"title": "Morse Code Translator",
325333
"description": "Translate text to Morse code and vice-versa, with audio playback.",
326334
"icon": "TranslateIcon",
335+
"pinned_order": 10,
327336
"created_at": "2025-11-23T18:16:28+03:00"
328337
},
329338
{
@@ -380,6 +389,7 @@
380389
"title": "Notepad",
381390
"description": "A simple, distraction-free text editor.",
382391
"icon": "NotePencilIcon",
392+
"pinned_order": 1,
383393
"created_at": "2025-11-27T14:00:00+03:00"
384394
},
385395
{
@@ -388,6 +398,7 @@
388398
"title": "Image Toolkit",
389399
"description": "A toolkit for basic image manipulations.",
390400
"icon": "ImageIcon",
401+
"pinned_order": 11,
391402
"created_at": "2025-11-10T23:58:16+03:00"
392403
},
393404
{
@@ -396,6 +407,7 @@
396407
"title": "Image Compressor",
397408
"description": "Compress images to reduce file size while maintaining quality.",
398409
"icon": "ArrowsInLineHorizontalIcon",
410+
"pinned_order": 12,
399411
"created_at": "2025-11-23T12:54:58+03:00"
400412
},
401413
{
@@ -404,6 +416,7 @@
404416
"title": "Stopwatch",
405417
"description": "A simple stopwatch with lap functionality.",
406418
"icon": "TimerIcon",
419+
"pinned_order": 13,
407420
"created_at": "2025-11-23T17:30:23+03:00"
408421
},
409422
{
@@ -412,6 +425,7 @@
412425
"title": "Pomodoro Timer",
413426
"description": "A simple and customizable Pomodoro timer to boost your productivity.",
414427
"icon": "TimerIcon",
428+
"pinned_order": 14,
415429
"created_at": "2025-11-23T18:11:03+03:00"
416430
},
417431
{

src/components/AnimatedRoutes.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const RoguelikeGamePage = lazy(() => import('../pages/apps/RoguelikeGamePage'));
7575
const TcgCardGeneratorPage = lazy(() => import('../pages/apps/TcgCardGeneratorPage'));
7676
const KeyboardTypingSpeedTesterPage = lazy(() => import('../pages/apps/KeyboardTypingSpeedTesterPage'));
7777
const NotepadPage = lazy(() => import('../pages/apps/NotepadPage'));
78+
const PinnedAppPage = lazy(() => import('../pages/PinnedAppPage'));
7879
const SettingsPage = lazy(() => import('../pages/SettingsPage'));
7980
const TimelinePage = lazy(() => import('../pages/TimelinePage'));
8081
const UsefulLinksPage = lazy(() => import('../pages/UsefulLinksPage'));
@@ -1424,6 +1425,24 @@ function AnimatedRoutes() {
14241425
path="/apps::np"
14251426
element={<Navigate to="/apps/notepad" replace />}
14261427
/>
1428+
<Route
1429+
path="/apps/pinned"
1430+
element={
1431+
<motion.div
1432+
initial="initial"
1433+
animate="in"
1434+
exit="out"
1435+
variants={pageVariants}
1436+
transition={pageTransition}
1437+
>
1438+
<PinnedAppPage />
1439+
</motion.div>
1440+
}
1441+
/>
1442+
<Route
1443+
path="/apps::pinned"
1444+
element={<Navigate to="/apps/pinned" replace />}
1445+
/>
14271446
{/* D&D specific 404 page */}
14281447
<Route
14291448
path="/stories/*"

src/pages/AppPage.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
MagnifyingGlassIcon,
88
CaretRight,
99
CaretDown,
10+
Star,
1011
} from '@phosphor-icons/react';
1112
import {motion, AnimatePresence} from 'framer-motion';
1213
import AppCard from '../components/AppCard';
@@ -265,7 +266,14 @@ function AppPage() {
265266
<div
266267
className="p-6 pt-0 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 border-t border-gray-800/50 mt-2 pt-6">
267268
{sortedApps.map((app, index) => (
268-
<AppCard key={app.slug || index} app={app}/>
269+
<div key={app.slug || index} className="relative">
270+
<AppCard app={app}/>
271+
{app.pinned_order && (
272+
<div className="absolute top-4 right-4 text-yellow-400 drop-shadow-md z-10 pointer-events-none">
273+
<Star weight="fill" size={24} />
274+
</div>
275+
)}
276+
</div>
269277
))}
270278
</div>
271279
</motion.div>

src/pages/PinnedAppPage.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, {useState, useEffect} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {ArrowLeftIcon, PushPin, Star, ArrowRight, Crown} from '@phosphor-icons/react';
4+
import {motion} from 'framer-motion';
5+
import useSeo from '../hooks/useSeo';
6+
import {appIcons} from '../utils/appIcons';
7+
8+
const PinnedAppCard = ({app, index}) => {
9+
const Icon = appIcons[app.icon] || Star;
10+
const isTop3 = index < 3;
11+
// Special styling for Top 3
12+
const rankColors = {
13+
0: 'text-yellow-400 border-yellow-500/50 bg-yellow-500/10', // Gold
14+
1: 'text-gray-300 border-gray-400/50 bg-gray-400/10', // Silver
15+
2: 'text-amber-600 border-amber-700/50 bg-amber-700/10', // Bronze
16+
};
17+
18+
const rankStyle = rankColors[index] || 'text-gray-500 border-gray-700/50 bg-gray-800/50';
19+
return (
20+
<Link to={app.to}
21+
className={`block group relative h-full ${isTop3 ? 'col-span-1 md:col-span-1 lg:col-span-1' : ''}`}>
22+
<div
23+
className={`relative flex flex-col h-full bg-gray-900/60 backdrop-blur-md border border-white/10 rounded-3xl p-6 hover:bg-gray-800/80 transition-all duration-300 overflow-hidden group-hover:border-primary-500/30 group-hover:shadow-2xl group-hover:shadow-primary-500/20 group-hover:-translate-y-2`}>
24+
{/* Watermark Icon */}
25+
<div
26+
className="absolute -bottom-10 -right-10 opacity-20 group-hover:opacity-30 transition-opacity duration-500 rotate-12 pointer-events-none text-white">
27+
<Icon size={220} weight="fill"/>
28+
</div>
29+
{/* Rank Badge */}
30+
<div className="flex justify-between items-start mb-6 relative z-10">
31+
<div
32+
className={`p-3 rounded-2xl bg-gray-800/80 border border-white/5 text-primary-400 group-hover:scale-110 transition-transform duration-300 shadow-inner`}>
33+
<Icon size={32} weight="duotone"/>
34+
</div>
35+
<div
36+
className={`flex items-center justify-center w-10 h-10 rounded-full border ${rankStyle} font-bold font-mono text-lg shadow-sm backdrop-blur-sm`}>
37+
{index < 3 && <Crown size={14} weight="fill" className="mr-1 -ml-1"/>}
38+
{app.pinned_order}
39+
</div>
40+
</div>
41+
<div className="relative z-10 flex-grow">
42+
<h3
43+
className={`font-bold text-white mb-3 group-hover:text-primary-400 transition-colors font-mono ${isTop3 ? 'text-2xl' : 'text-xl'}`}>
44+
{app.title}
45+
</h3>
46+
<p className="text-gray-400 text-sm leading-relaxed line-clamp-3 group-hover:text-gray-300 transition-colors">
47+
{app.description}
48+
</p>
49+
</div>
50+
<div
51+
className="relative z-10 mt-6 flex items-center text-sm font-medium text-primary-400 group-hover:text-primary-300 transition-colors">
52+
Launch App <ArrowRight size={16} className="ml-2 transition-transform group-hover:translate-x-1"/>
53+
</div>
54+
</div>
55+
</Link>
56+
);
57+
};
58+
const PinnedAppPage = () => {
59+
useSeo({
60+
title: 'Pinned Apps | Fezcodex',
61+
description: 'A curated selection of my favorite and most used apps.',
62+
keywords: ['pinned', 'favorite', 'apps', 'tools', 'hall of fame'],
63+
});
64+
65+
const [pinnedApps, setPinnedApps] = useState([]);
66+
const [isLoading, setIsLoading] = useState(true);
67+
useEffect(() => {
68+
fetch('/apps/apps.json')
69+
.then((res) => res.json())
70+
.then((data) => {
71+
const allApps = Object.values(data).flatMap((cat) => categoryToApps(cat));
72+
const pinned = allApps
73+
.filter((app) => app.pinned_order)
74+
.sort((a, b) => a.pinned_order - b.pinned_order);
75+
setPinnedApps(pinned);
76+
})
77+
.catch((err) => console.error(err))
78+
.finally(() => setIsLoading(false));
79+
}, []);
80+
81+
const categoryToApps = (category) => {
82+
return category.apps.map(app => ({...app, categoryName: category.name}));
83+
};
84+
85+
return (
86+
<div className="min-h-screen bg-gray-950 py-16 sm:py-24 relative overflow-hidden">
87+
{/* Background Glows */}
88+
<div
89+
className="absolute top-0 left-1/2 -translate-x-1/2 w-3/4 h-[500px] bg-primary-500/10 blur-[120px] rounded-full pointer-events-none mix-blend-screen"/>
90+
<div
91+
className="absolute bottom-0 right-0 w-[600px] h-[600px] bg-blue-600/10 blur-[120px] rounded-full pointer-events-none mix-blend-screen"/>
92+
93+
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
94+
<div className="relative mb-16 text-center">
95+
<div className="absolute left-0 top-1/2 -translate-y-1/2 hidden md:block">
96+
<Link
97+
to="/"
98+
className="group text-primary-400 hover:text-primary-300 hover:underline flex items-center gap-2 text-sm transition-colors"
99+
>
100+
<ArrowLeftIcon size={18} className="transition-transform group-hover:-translate-x-1"/> Home
101+
</Link>
102+
</div>
103+
104+
<motion.div
105+
106+
initial={{opacity: 0, y: 20}}
107+
108+
animate={{opacity: 1, y: 0}}
109+
110+
transition={{duration: 0.5}}
111+
112+
>
113+
114+
<h1 className="text-5xl md:text-6xl font-bold tracking-tight text-white font-mono mb-6">
115+
116+
Hall of <span
117+
className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-500">Fame</span>
118+
119+
</h1>
120+
121+
<p className="text-lg text-gray-400 max-w-2xl mx-auto font-light">
122+
123+
The essential toolkit. Hand-picked and pinned for quick access.
124+
125+
</p>
126+
127+
</motion.div>
128+
</div>
129+
130+
{isLoading ? (
131+
132+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
133+
134+
{[...Array(8)].map((_, i) => (
135+
136+
<div key={i} className="bg-gray-900/30 border border-gray-800 rounded-3xl h-72 animate-pulse"></div>
137+
138+
))}
139+
140+
</div>
141+
142+
) : (
143+
144+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr">
145+
146+
{pinnedApps.map((app, index) => (
147+
148+
<motion.div
149+
150+
key={app.slug}
151+
152+
initial={{opacity: 0, scale: 0.95}}
153+
154+
animate={{opacity: 1, scale: 1}}
155+
156+
transition={{duration: 0.3, delay: index * 0.05}}
157+
158+
>
159+
160+
<PinnedAppCard app={app} index={index}/>
161+
162+
</motion.div>
163+
164+
))}
165+
166+
</div>
167+
168+
)}
169+
</div>
170+
</div>
171+
);
172+
};
173+
174+
export default PinnedAppPage;

0 commit comments

Comments
 (0)