Skip to content

Commit a69778d

Browse files
committed
feat(formatter): simplify JSON editing flow
1 parent 4357191 commit a69778d

6 files changed

Lines changed: 151 additions & 214 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react';
2-
import { JsonFormatter, GitHubIcon } from './components';
2+
import { GitHubIcon, JsonFormatter } from '@components';
33
import './App.less';
44

55
export default function App() {

src/components/json-formatter/index.tsx

Lines changed: 47 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,90 @@
1-
import { useState, useCallback, useEffect } from 'react';
1+
import { useState, useCallback, useMemo } from 'react';
22
import Editor from '@monaco-editor/react';
3-
import { DEFAULT_JSON } from '../../constants';
4-
import { compress, decompress } from '../../utils';
3+
import { DEFAULT_JSON } from '@constants';
4+
import { validateJson } from '@utils';
55
import './style.less';
66

7-
const URL_PARAM = 'json';
8-
97
interface JsonFormatterProps {
108
isDarkMode: boolean;
119
onThemeChange: (isDark: boolean) => void;
1210
}
1311

1412
export default function JsonFormatter({ isDarkMode, onThemeChange }: JsonFormatterProps) {
15-
const [inputJson, setInputJson] = useState(DEFAULT_JSON);
16-
const [outputJson, setOutputJson] = useState('');
13+
const [jsonText, setJsonText] = useState(DEFAULT_JSON);
1714
const [error, setError] = useState('');
1815
const [success, setSuccess] = useState('');
19-
const [hasInitializedFromUrl, setHasInitializedFromUrl] = useState(false);
16+
const jsonValidation = useMemo(() => validateJson(jsonText), [jsonText]);
2017

21-
const applyInputValue = useCallback((value: string) => {
22-
setInputJson(value);
18+
const applyJsonValue = useCallback((value: string) => {
19+
setJsonText(value);
20+
setError('');
2321
setSuccess('');
24-
25-
try {
26-
const parsed = JSON.parse(value);
27-
const formatted = JSON.stringify(parsed, null, 2);
28-
setOutputJson(formatted);
29-
setError('');
30-
} catch {
31-
if (value.trim()) {
32-
setError('');
33-
setOutputJson('');
34-
} else {
35-
setError('');
36-
setOutputJson('');
37-
}
38-
}
3922
}, []);
4023

4124
const formatJson = useCallback(() => {
4225
try {
43-
const parsed = JSON.parse(inputJson);
26+
const parsed = JSON.parse(jsonText);
4427
const formatted = JSON.stringify(parsed, null, 2);
45-
setOutputJson(formatted);
28+
setJsonText(formatted);
4629
setError('');
4730
setSuccess('JSON formatted successfully!');
4831
setTimeout(() => setSuccess(''), 3000);
4932
} catch (err) {
5033
setError(`Invalid JSON: ${(err as Error).message}`);
51-
setOutputJson('');
5234
setSuccess('');
5335
}
54-
}, [inputJson]);
36+
}, [jsonText]);
5537

56-
const handleInputChange = useCallback((value: string | undefined) => {
57-
applyInputValue(value || '');
58-
}, [applyInputValue]);
38+
const handleInputChange = useCallback(
39+
(value: string | undefined) => {
40+
applyJsonValue(value || '');
41+
},
42+
[applyJsonValue],
43+
);
5944

6045
const minifyJson = useCallback(() => {
6146
try {
62-
const parsed = JSON.parse(inputJson);
47+
const parsed = JSON.parse(jsonText);
6348
const minified = JSON.stringify(parsed);
64-
setOutputJson(minified);
49+
setJsonText(minified);
6550
setError('');
6651
setSuccess('JSON minified successfully!');
6752
setTimeout(() => setSuccess(''), 3000);
6853
} catch (err) {
6954
setError(`Invalid JSON: ${(err as Error).message}`);
70-
setOutputJson('');
7155
setSuccess('');
7256
}
73-
}, [inputJson]);
57+
}, [jsonText]);
7458

7559
const clearAll = useCallback(() => {
76-
setInputJson('');
77-
setOutputJson('');
60+
setJsonText('');
7861
setError('');
7962
setSuccess('');
8063
}, []);
8164

8265
const copyToClipboard = useCallback(async () => {
83-
if (outputJson) {
66+
if (jsonText) {
8467
try {
85-
await navigator.clipboard.writeText(outputJson);
68+
await navigator.clipboard.writeText(jsonText);
8669
setSuccess('Copied to clipboard!');
8770
setTimeout(() => setSuccess(''), 3000);
8871
} catch {
8972
setError('Failed to copy to clipboard');
9073
}
9174
}
92-
}, [outputJson]);
93-
94-
const copyShareUrl = useCallback(async () => {
95-
if (!inputJson) {
96-
return;
97-
}
98-
99-
try {
100-
const url = new URL(window.location.href);
101-
const encoded = await compress(inputJson);
102-
const hashParams = new URLSearchParams(url.hash.slice(1));
103-
hashParams.set(URL_PARAM, encoded);
104-
url.hash = hashParams.toString();
105-
url.searchParams.delete(URL_PARAM);
106-
107-
await navigator.clipboard.writeText(url.toString());
108-
setSuccess('Share URL copied!');
109-
setTimeout(() => setSuccess(''), 3000);
110-
} catch {
111-
setError('Failed to copy share URL');
112-
setTimeout(() => setError(''), 3000);
113-
}
114-
}, [inputJson]);
115-
116-
useEffect(() => {
117-
let cancelled = false;
118-
119-
const loadFromUrl = async () => {
120-
const hashParams = new URLSearchParams(window.location.hash.slice(1));
121-
let encoded = hashParams.get(URL_PARAM);
122-
123-
if (!encoded) {
124-
const searchParams = new URLSearchParams(window.location.search);
125-
encoded = searchParams.get(URL_PARAM);
126-
}
127-
128-
if (!encoded) {
129-
setHasInitializedFromUrl(true);
130-
return;
131-
}
132-
133-
try {
134-
const text = await decompress(encoded);
135-
if (!cancelled) {
136-
applyInputValue(text);
137-
}
138-
} catch {
139-
if (!cancelled) {
140-
setError('Failed to read JSON from URL');
141-
setTimeout(() => setError(''), 3000);
142-
}
143-
} finally {
144-
if (!cancelled) {
145-
setHasInitializedFromUrl(true);
146-
}
147-
}
148-
};
149-
150-
loadFromUrl();
151-
152-
return () => {
153-
cancelled = true;
154-
};
155-
}, [applyInputValue]);
156-
157-
useEffect(() => {
158-
if (!hasInitializedFromUrl) {
159-
return;
160-
}
161-
162-
let cancelled = false;
75+
}, [jsonText]);
16376

164-
const syncToUrl = async () => {
77+
const downloadJson = useCallback(() => {
78+
if (jsonText) {
16579
try {
166-
const url = new URL(window.location.href);
167-
const hashParams = new URLSearchParams(url.hash.slice(1));
168-
169-
if (!inputJson) {
170-
hashParams.delete(URL_PARAM);
171-
} else {
172-
const encoded = await compress(inputJson);
173-
if (cancelled) {
174-
return;
175-
}
176-
hashParams.set(URL_PARAM, encoded);
177-
}
178-
179-
url.hash = hashParams.toString();
180-
url.searchParams.delete(URL_PARAM);
181-
182-
if (!cancelled) {
183-
window.history.replaceState(null, '', url.toString());
184-
}
185-
} catch {
186-
if (!cancelled) {
187-
setError('Failed to sync JSON to URL');
188-
setTimeout(() => setError(''), 3000);
189-
}
80+
JSON.parse(jsonText);
81+
} catch (err) {
82+
setError(`Invalid JSON: ${(err as Error).message}`);
83+
setSuccess('');
84+
return;
19085
}
191-
};
192-
193-
syncToUrl();
194-
195-
return () => {
196-
cancelled = true;
197-
};
198-
}, [inputJson, hasInitializedFromUrl]);
19986

200-
const downloadJson = useCallback(() => {
201-
if (outputJson) {
202-
const blob = new Blob([outputJson], { type: 'application/json' });
87+
const blob = new Blob([jsonText], { type: 'application/json' });
20388
const url = URL.createObjectURL(blob);
20489
const a = document.createElement('a');
20590
a.href = url;
@@ -211,13 +96,16 @@ export default function JsonFormatter({ isDarkMode, onThemeChange }: JsonFormatt
21196
setSuccess('JSON downloaded successfully!');
21297
setTimeout(() => setSuccess(''), 3000);
21398
}
214-
}, [outputJson]);
99+
}, [jsonText]);
215100

216101
const toggleTheme = useCallback(() => {
217102
onThemeChange(!isDarkMode);
218103
}, [isDarkMode, onThemeChange]);
219104

220105
const themeClass = isDarkMode ? 'dark' : 'light';
106+
const validationError =
107+
jsonValidation.status === 'invalid' ? `Invalid JSON: ${jsonValidation.message}` : '';
108+
const activeError = error || validationError;
221109

222110
return (
223111
<div className={`formatter-container ${themeClass}`}>
@@ -232,23 +120,15 @@ export default function JsonFormatter({ isDarkMode, onThemeChange }: JsonFormatt
232120
<button
233121
className={`button ${themeClass}`}
234122
onClick={copyToClipboard}
235-
disabled={!outputJson}
123+
disabled={!jsonText}
236124
type="button"
237125
>
238126
copy
239127
</button>
240-
<button
241-
className={`button ${themeClass}`}
242-
onClick={copyShareUrl}
243-
disabled={!inputJson}
244-
type="button"
245-
>
246-
share
247-
</button>
248128
<button
249129
className={`button ${themeClass}`}
250130
onClick={downloadJson}
251-
disabled={!outputJson}
131+
disabled={!jsonText}
252132
type="button"
253133
>
254134
download
@@ -268,16 +148,22 @@ export default function JsonFormatter({ isDarkMode, onThemeChange }: JsonFormatt
268148
</div>
269149
</div>
270150

271-
{error && <div className={`error-message ${themeClass}`}>{error}</div>}
151+
{activeError && <div className={`error-message ${themeClass}`}>{activeError}</div>}
272152
{success && <div className={`success-message ${themeClass}`}>{success}</div>}
273153

274154
<div className="editor-section">
275155
<div className={`editor-panel ${themeClass}`}>
276-
<div className={`panel-header ${themeClass}`}>input</div>
156+
<div className={`panel-header ${themeClass}`}>
157+
<span>json</span>
158+
<span className={`validation-status ${themeClass} ${jsonValidation.status}`}>
159+
<span className="validation-dot" />
160+
{jsonValidation.message}
161+
</span>
162+
</div>
277163
<Editor
278164
height="100%"
279165
defaultLanguage="json"
280-
value={inputJson}
166+
value={jsonText}
281167
onChange={handleInputChange}
282168
theme={isDarkMode ? 'vs-dark' : 'vs'}
283169
options={{
@@ -291,26 +177,6 @@ export default function JsonFormatter({ isDarkMode, onThemeChange }: JsonFormatt
291177
}}
292178
/>
293179
</div>
294-
295-
<div className={`editor-panel ${themeClass}`}>
296-
<div className={`panel-header ${themeClass}`}>output</div>
297-
<Editor
298-
height="100%"
299-
defaultLanguage="json"
300-
value={outputJson}
301-
theme={isDarkMode ? 'vs-dark' : 'vs'}
302-
options={{
303-
readOnly: true,
304-
minimap: { enabled: false },
305-
scrollBeyondLastLine: false,
306-
fontSize: 13,
307-
lineNumbers: 'on',
308-
wordWrap: 'on',
309-
tabSize: 2,
310-
fontFamily: 'JetBrains Mono, Fira Code, Cascadia Code, SF Mono, Consolas, monospace',
311-
}}
312-
/>
313-
</div>
314180
</div>
315181
</div>
316182
);

0 commit comments

Comments
 (0)