Skip to content

Commit 4016765

Browse files
committed
new(app): json formatter
1 parent bd8dfd9 commit 4016765

File tree

4 files changed

+218
-1
lines changed

4 files changed

+218
-1
lines changed

src/components/AnimatedRoutes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import PickerWheelPage from '../pages/apps/PickerWheelPage';
3434
import CodenameGeneratorPage from '../pages/apps/CodenameGeneratorPage';
3535
import ImageToolkitPage from '../pages/apps/ImageToolkitPage';
3636
import PasswordGeneratorPage from '../pages/apps/PasswordGeneratorPage';
37+
import JsonFormatterPage from '../pages/apps/JsonFormatterPage';
3738

3839
import UsefulLinksPage from '../pages/UsefulLinksPage';
3940

@@ -320,6 +321,7 @@ function AnimatedRoutes() {
320321
<Route path="/apps::cg" element={<Navigate to="/apps/codename-generator" replace />} />
321322
<Route path="/apps::itk" element={<Navigate to="/apps/image-toolkit" replace />} />
322323
<Route path="/apps::pg" element={<Navigate to="/apps/password-generator" replace />} />
324+
<Route path="/apps::jf" element={<Navigate to="/apps/json-formatter" replace />} />
323325
{/* End of hardcoded redirects */}
324326
<Route
325327
path="/apps/ip"
@@ -560,6 +562,20 @@ function AnimatedRoutes() {
560562
</motion.div>
561563
}
562564
/>
565+
<Route
566+
path="/apps/json-formatter"
567+
element={
568+
<motion.div
569+
initial="initial"
570+
animate="in"
571+
exit="out"
572+
variants={pageVariants}
573+
transition={pageTransition}
574+
>
575+
<JsonFormatterPage />
576+
</motion.div>
577+
}
578+
/>
563579
{/* D&D specific 404 page */}
564580
<Route
565581
path="/dnd/*"

src/pages/AppPage.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ const apps = [
8989
description: 'Generate strong, random passwords with customizable options.',
9090
icon: Key,
9191
},
92+
{
93+
to: '/apps/json-formatter',
94+
title: 'JSON Formatter & Validator',
95+
description: 'Format and validate JSON data for readability and correctness.',
96+
icon: Code,
97+
},
9298
{
9399
to: '/apps/color-palette-generator',
94100
title: 'Color Palette Generator',
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React, { useState, useCallback } from 'react';
2+
import usePageTitle from '../../utils/usePageTitle';
3+
import { Link } from 'react-router-dom';
4+
import { ArrowLeftIcon, Code, CopySimple } from '@phosphor-icons/react';
5+
import colors from '../../config/colors';
6+
import { useToast } from '../../hooks/useToast';
7+
import '../../styles/app-buttons.css';
8+
9+
const JsonFormatterPage = () => {
10+
usePageTitle('JSON Formatter');
11+
const { addToast } = useToast();
12+
13+
const [jsonInput, setJsonInput] = useState('');
14+
const [jsonOutput, setJsonOutput] = useState('');
15+
const [validationMessage, setValidationMessage] = useState('');
16+
17+
const cardStyle = {
18+
backgroundColor: colors['app-alpha-10'],
19+
borderColor: colors['app-alpha-50'],
20+
color: colors.app,
21+
};
22+
23+
const textareaStyle = `mt-1 block w-full p-3 border rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 border-gray-600 font-mono text-sm`;
24+
const buttonStyle = `px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out roll-button`;
25+
26+
const formatJson = useCallback(() => {
27+
setValidationMessage('');
28+
if (!jsonInput.trim()) {
29+
setJsonOutput('');
30+
addToast({ title: 'Input Empty', message: 'Please enter JSON to format.', duration: 3000, type: 'info' });
31+
return;
32+
}
33+
try {
34+
const parsed = JSON.parse(jsonInput);
35+
setJsonOutput(JSON.stringify(parsed, null, 2)); // Pretty print with 2 spaces
36+
addToast({ title: 'Formatted', message: 'JSON formatted successfully.', duration: 2000 });
37+
} catch (error) {
38+
setJsonOutput('');
39+
setValidationMessage(`Invalid JSON: ${error.message}`);
40+
addToast({ title: 'Error', message: 'Invalid JSON input.', duration: 3000, type: 'error' });
41+
}
42+
}, [jsonInput, addToast]);
43+
44+
const validateJson = useCallback(() => {
45+
setJsonOutput('');
46+
if (!jsonInput.trim()) {
47+
setValidationMessage('Please enter JSON to validate.');
48+
addToast({ title: 'Input Empty', message: 'Please enter JSON to validate.', duration: 3000, type: 'info' });
49+
return;
50+
}
51+
try {
52+
JSON.parse(jsonInput);
53+
setValidationMessage('Valid JSON!');
54+
addToast({ title: 'Valid', message: 'JSON is valid.', duration: 2000 });
55+
} catch (error) {
56+
setValidationMessage(`Invalid JSON: ${error.message}`);
57+
addToast({ title: 'Error', message: 'Invalid JSON input.', duration: 3000, type: 'error' });
58+
}
59+
}, [jsonInput, addToast]);
60+
61+
const copyToClipboard = useCallback(() => {
62+
const textToCopy = jsonOutput || jsonInput; // Copy formatted if available, else raw input
63+
if (textToCopy.trim()) {
64+
navigator.clipboard.writeText(textToCopy);
65+
addToast({ title: 'Copied!', message: 'Content copied to clipboard.', duration: 2000 });
66+
} else {
67+
addToast({ title: 'Cannot Copy', message: 'No content to copy.', duration: 2000, type: 'error' });
68+
}
69+
}, [jsonInput, jsonOutput, addToast]);
70+
71+
return (
72+
<div className="py-16 sm:py-24">
73+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
74+
<Link
75+
to="/apps"
76+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
77+
>
78+
<ArrowLeftIcon size={24} /> Back to Apps
79+
</Link>
80+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
81+
<span className="codex-color">fc</span>
82+
<span className="separator-color">::</span>
83+
<span className="apps-color">apps</span>
84+
<span className="separator-color">::</span>
85+
<span className="single-app-color">jf</span>
86+
</h1>
87+
<hr className="border-gray-700" />
88+
<div className="flex justify-center items-center mt-16">
89+
<div
90+
className="group bg-transparent border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform overflow-hidden h-full w-full max-w-4xl"
91+
style={cardStyle}
92+
>
93+
<div
94+
className="absolute top-0 left-0 w-full h-full opacity-10"
95+
style={{
96+
backgroundImage:
97+
'radial-gradient(circle, white 1px, transparent 1px)',
98+
backgroundSize: '10px 10px',
99+
}}
100+
></div>
101+
<div className="relative z-10 p-1">
102+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app"> JSON Formatter & Validator </h1>
103+
<hr className="border-gray-700 mb-4" />
104+
105+
{/* Client-Side Notification */}
106+
<div className="bg-yellow-900 bg-opacity-30 border border-yellow-700 text-yellow-300 px-4 py-3 rounded relative mb-6" role="alert">
107+
<strong className="font-bold">Client-Side Only:</strong>
108+
<span className="block sm:inline ml-2">This tool operates entirely within your browser. No JSON data is sent to any server, ensuring maximum privacy.</span>
109+
</div>
110+
111+
<div className="mb-6">
112+
<label htmlFor="jsonInput" className="block text-sm font-medium text-gray-300 mb-2">
113+
JSON Input
114+
</label>
115+
<textarea
116+
id="jsonInput"
117+
rows="10"
118+
className={textareaStyle}
119+
value={jsonInput}
120+
onChange={(e) => setJsonInput(e.target.value)}
121+
placeholder='Enter your JSON here, e.g., {"name": "John Doe", "age": 30}'
122+
></textarea>
123+
</div>
124+
125+
<div className="flex flex-col sm:flex-row gap-4 mb-6">
126+
<button
127+
onClick={formatJson}
128+
className={`${buttonStyle} w-full sm:w-1/2`}
129+
style={{
130+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
131+
color: cardStyle.color,
132+
borderColor: cardStyle.borderColor,
133+
border: '1px solid',
134+
}}
135+
>
136+
Format JSON
137+
</button>
138+
<button
139+
onClick={validateJson}
140+
className={`${buttonStyle} w-full sm:w-1/2`}
141+
style={{
142+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
143+
color: cardStyle.color,
144+
borderColor: cardStyle.borderColor,
145+
border: '1px solid',
146+
}}
147+
>
148+
Validate JSON
149+
</button>
150+
</div>
151+
152+
{validationMessage && (
153+
<div className={`px-4 py-3 rounded relative mb-6 ${validationMessage.startsWith('Valid') ? 'bg-green-900 bg-opacity-30 border border-green-700 text-green-300' : 'bg-red-900 bg-opacity-30 border border-red-700 text-red-300'}`} role="alert">
154+
{validationMessage}
155+
</div>
156+
)}
157+
158+
{jsonOutput && (
159+
<div className="mb-6">
160+
<label htmlFor="jsonOutput" className="block text-sm font-medium text-gray-300 mb-2">
161+
Formatted JSON
162+
</label>
163+
<div className="flex">
164+
<textarea
165+
id="jsonOutput"
166+
rows="10"
167+
className={`${textareaStyle} flex-grow`}
168+
value={jsonOutput}
169+
readOnly
170+
></textarea>
171+
<button
172+
onClick={copyToClipboard}
173+
className="ml-2 px-4 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out roll-button flex items-center justify-center"
174+
style={{
175+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
176+
color: cardStyle.color,
177+
borderColor: cardStyle.borderColor,
178+
border: '1px solid',
179+
}}
180+
title="Copy to Clipboard"
181+
>
182+
<CopySimple size={20} />
183+
</button>
184+
</div>
185+
</div>
186+
)}
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
</div>
192+
);
193+
};
194+
195+
export default JsonFormatterPage;

src/pages/apps/PasswordGeneratorPage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ const PasswordGeneratorPage = () => {
7575
<span className="separator-color">::</span>
7676
<span className="apps-color">apps</span>
7777
<span className="separator-color">::</span>
78-
<span className="single-app-color">password</span>
78+
<span className="single-app-color">pg</span>
7979
</h1>
8080
<hr className="border-gray-700" />
8181
<div className="flex justify-center items-center mt-16">

0 commit comments

Comments
 (0)