1- import { useState , useCallback , useEffect } from 'react' ;
1+ import { useState , useCallback , useMemo } from 'react' ;
22import 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' ;
55import './style.less' ;
66
7- const URL_PARAM = 'json' ;
8-
97interface JsonFormatterProps {
108 isDarkMode : boolean ;
119 onThemeChange : ( isDark : boolean ) => void ;
1210}
1311
1412export 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