Skip to content

Commit dcd9db1

Browse files
committed
app: notebook.
1 parent 7b67083 commit dcd9db1

File tree

4 files changed

+224
-1
lines changed

4 files changed

+224
-1
lines changed

public/apps/apps.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,14 @@
452452
"description": "Tap the beat to guess the BPM of a song.",
453453
"icon": "MetronomeIcon",
454454
"created_at": "2025-11-25T16:14:05+03:00"
455+
},
456+
{
457+
"slug": "notepad",
458+
"to": "/apps/notepad",
459+
"title": "Notepad",
460+
"description": "A simple, distraction-free text editor.",
461+
"icon": "NotePencilIcon",
462+
"created_at": "2025-11-27T14:00:00+03:00"
455463
}
456464
]
457465
}

src/components/AnimatedRoutes.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const FootballEmblemCreatorPage = lazy(() => import('../pages/apps/FootballEmble
7474
const RoguelikeGamePage = lazy(() => import('../pages/apps/RoguelikeGamePage'));
7575
const TcgCardGeneratorPage = lazy(() => import('../pages/apps/TcgCardGeneratorPage'));
7676
const KeyboardTypingSpeedTesterPage = lazy(() => import('../pages/apps/KeyboardTypingSpeedTesterPage'));
77+
const NotepadPage = lazy(() => import('../pages/apps/NotepadPage'));
7778
const SettingsPage = lazy(() => import('../pages/SettingsPage'));
7879
const TimelinePage = lazy(() => import('../pages/TimelinePage'));
7980
const UsefulLinksPage = lazy(() => import('../pages/UsefulLinksPage'));
@@ -1405,7 +1406,25 @@ function AnimatedRoutes() {
14051406
</motion.div>
14061407
}
14071408
/>
1408-
{/* D&D specific 404 page */}
1409+
<Route
1410+
path="/apps/notepad"
1411+
element={
1412+
<motion.div
1413+
initial="initial"
1414+
animate="in"
1415+
exit="out"
1416+
variants={pageVariants}
1417+
transition={pageTransition}
1418+
>
1419+
<NotepadPage />
1420+
</motion.div>
1421+
}
1422+
/>
1423+
<Route
1424+
path="/apps::np"
1425+
element={<Navigate to="/apps/notepad" replace />}
1426+
/>
1427+
{/* D&D specific 404 page */}
14091428
<Route
14101429
path="/stories/*"
14111430
element={

src/pages/apps/NotepadPage.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React, {useState, useEffect, useRef} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {ArrowLeftIcon, DownloadSimple, Trash, CloudRain, FloppyDisk, FolderOpen} from '@phosphor-icons/react';
4+
import useSeo from '../../hooks/useSeo';
5+
import {useToast} from '../../hooks/useToast';
6+
7+
const NotepadPage = () => {
8+
useSeo({
9+
title: 'Notepad | Fezcodex',
10+
description: 'A simple, distraction-free notepad for your thoughts.',
11+
keywords: ['notepad', 'writing', 'text editor', 'simple', 'rain'],
12+
});
13+
14+
const [text, setText] = useState('');
15+
const [isRainy, setIsRainy] = useState(false);
16+
const textareaRef = useRef(null);
17+
const {addToast} = useToast();
18+
19+
// Load saved text from local storage on mount
20+
useEffect(() => {
21+
const savedText = localStorage.getItem('fezcodex-notepad-content');
22+
if (savedText) {
23+
setText(savedText);
24+
}
25+
}, []);
26+
27+
// Save text to local storage whenever it changes (Autosave)
28+
useEffect(() => {
29+
localStorage.setItem('fezcodex-notepad-content', text);
30+
}, [text]);
31+
32+
const handleSave = () => {
33+
localStorage.setItem('fezcodex-notepad-content', text);
34+
addToast({title: 'Saved', message: 'Note manually saved to local storage.', duration: 3000});
35+
};
36+
37+
const handleLoad = () => {
38+
const savedText = localStorage.getItem('fezcodex-notepad-content');
39+
if (savedText) {
40+
setText(savedText);
41+
addToast({title: 'Loaded', message: 'Note loaded from local storage.', duration: 3000});
42+
} else {
43+
addToast({title: 'Info', message: 'No saved note found.', duration: 3000});
44+
}
45+
};
46+
47+
const handleDownload = () => {
48+
const element = document.createElement('a');
49+
const file = new Blob([text], {type: 'text/plain'});
50+
element.href = URL.createObjectURL(file);
51+
element.download = 'note.txt';
52+
document.body.appendChild(element);
53+
element.click();
54+
document.body.removeChild(element);
55+
addToast({title: 'Downloaded', message: 'Your note has been saved.', duration: 3000});
56+
};
57+
58+
const handleClear = () => {
59+
if (window.confirm('Are you sure you want to clear your note?')) {
60+
setText('');
61+
addToast({title: 'Cleared', message: 'Notepad cleared.', duration: 3000});
62+
}
63+
};
64+
65+
const toggleRain = () => {
66+
setIsRainy(!isRainy);
67+
addToast({
68+
title: isRainy ? 'Sunlight' : 'Rainy Mood',
69+
message: isRainy ? 'Rain effect disabled.' : 'Rain effect enabled.',
70+
duration: 2000
71+
});
72+
};
73+
74+
return (
75+
<div
76+
className={`min-h-screen flex flex-col transition-colors duration-500 ${isRainy ? 'bg-gray-900' : 'bg-[#fdfbf7]'}`}>
77+
{/* Rain Effect Overlay */}
78+
{isRainy && (
79+
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
80+
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-gray-900/80"></div>
81+
{/* Simple CSS Rain */}
82+
{[...Array(20)].map((_, i) => (
83+
<div
84+
key={i}
85+
className="absolute w-px bg-blue-400/30 animate-rain"
86+
style={{
87+
left: `${Math.random() * 100}%`,
88+
top: `-${Math.random() * 20}%`,
89+
height: `${Math.random() * 20 + 10}%`,
90+
animationDuration: `${Math.random() * 1 + 0.5}s`,
91+
animationDelay: `${Math.random() * 2}s`
92+
}}
93+
/>
94+
))}
95+
</div>
96+
)}
97+
98+
<div className="container mx-auto px-4 py-8 flex-grow flex flex-col relative z-10 max-w-4xl">
99+
{/* Header */}
100+
<div className="flex justify-between items-center mb-6">
101+
<Link to="/apps"
102+
className={`flex items-center gap-2 hover:underline ${isRainy ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'}`}>
103+
<ArrowLeftIcon/> Back to Apps
104+
</Link>
105+
<h1 className={`text-2xl font-serif font-bold tracking-wide ${isRainy ? 'text-gray-200' : 'text-gray-800'}`}>
106+
Notepad
107+
</h1>
108+
<div className="flex gap-2">
109+
<button
110+
onClick={handleSave}
111+
className={`p-2 rounded-full transition-colors ${isRainy ? 'text-cyan-400 hover:bg-cyan-900/30' : 'text-cyan-600 hover:bg-cyan-100'}`}
112+
title="Save to Local Storage"
113+
>
114+
<FloppyDisk size={20} />
115+
</button>
116+
<button
117+
onClick={handleLoad}
118+
className={`p-2 rounded-full transition-colors ${isRainy ? 'text-amber-400 hover:bg-amber-900/30' : 'text-amber-600 hover:bg-amber-100'}`}
119+
title="Load from Local Storage"
120+
>
121+
<FolderOpen size={20} />
122+
</button>
123+
<button
124+
onClick={toggleRain}
125+
className={`p-2 rounded-full transition-colors ${isRainy ? 'text-blue-300 bg-blue-900/30 hover:bg-blue-900/50' : 'text-gray-500 hover:bg-gray-200'}`}
126+
title="Toggle Rain"
127+
>
128+
<CloudRain size={20} weight={isRainy ? "fill" : "regular"}/>
129+
</button>
130+
<button
131+
onClick={handleClear}
132+
className={`p-2 rounded-full transition-colors ${isRainy ? 'text-red-400 hover:bg-red-900/30' : 'text-red-500 hover:bg-red-100'}`}
133+
title="Clear Text"
134+
>
135+
<Trash size={20}/>
136+
</button>
137+
<button
138+
onClick={handleDownload}
139+
className={`p-2 rounded-full transition-colors ${isRainy ? 'text-green-400 hover:bg-green-900/30' : 'text-green-600 hover:bg-green-100'}`}
140+
title="Download Text"
141+
>
142+
<DownloadSimple size={20}/>
143+
</button>
144+
</div>
145+
</div>
146+
147+
{/* Notepad Area */}
148+
<div
149+
className={`flex-grow rounded-lg shadow-lg relative overflow-hidden transition-all duration-500 ${isRainy ? 'bg-gray-800 border border-gray-700' : 'bg-white border border-gray-200'}`}>
150+
{/* Paper Lines Background */}
151+
<div
152+
className="absolute inset-0 pointer-events-none opacity-50"
153+
style={{
154+
backgroundImage: `linear-gradient(${isRainy ? '#374151' : '#e5e7eb'} 1px, transparent 1px)`,
155+
backgroundSize: '100% 2rem',
156+
marginTop: '2rem' // Offset for top padding
157+
}}
158+
></div>
159+
160+
{/* Red Margin Line */}
161+
<div
162+
className={`absolute top-0 bottom-0 left-12 w-px ${isRainy ? 'bg-red-900/50' : 'bg-red-200'} pointer-events-none`}></div>
163+
164+
<textarea
165+
ref={textareaRef}
166+
className={`w-full h-full p-8 pl-16 text-lg font-mono leading-8 bg-transparent border-none resize-none focus:ring-0 focus:outline-none relative z-10 ${isRainy ? 'text-gray-300 placeholder-gray-600' : 'text-gray-800 placeholder-gray-400'}`}
167+
placeholder="Start typing..."
168+
value={text}
169+
onChange={(e) => setText(e.target.value)}
170+
spellCheck="false"
171+
/>
172+
</div>
173+
174+
<div className={`mt-2 text-right text-sm font-mono ${isRainy ? 'text-gray-500' : 'text-gray-400'}`}>
175+
{text.length} chars | {text.split(/\s+/).filter((w) => w.length > 0).length} words
176+
</div>
177+
</div>
178+
179+
<style>{`
180+
@keyframes rain {
181+
0% { transform: translateY(0); }
182+
100% { transform: translateY(100vh); }
183+
}
184+
.animate-rain {
185+
animation-name: rain;
186+
animation-timing-function: linear;
187+
animation-iteration-count: infinite;
188+
}
189+
`}</style>
190+
</div>
191+
);
192+
};
193+
194+
export default NotepadPage;

src/utils/appIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
PencilSimpleIcon,
4747
TrophyIcon,
4848
CubeIcon,
49+
NotePencilIcon,
4950
} from '@phosphor-icons/react';
5051

5152
export const appIcons = {
@@ -96,4 +97,5 @@ export const appIcons = {
9697
PencilSimpleIcon,
9798
TrophyIcon,
9899
CubeIcon,
100+
NotePencilIcon,
99101
};

0 commit comments

Comments
 (0)