import React, { useState, useEffect } from 'react'; import { useTranslation } from '../js/i18n'; import SEO from '../components/SEO'; import styled from 'styled-components'; import { usePageLoading } from '../hooks/usePageLoading'; import LoadingOverlay from './LoadingOverlay'; const Container = styled.div` min-height: 100vh; background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); padding: 6rem 2rem 2rem; position: relative; &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px), linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px); background-size: 20px 20px; pointer-events: none; } `; const ContentWrapper = styled.div` max-width: 1400px; margin: 0 auto; position: relative; z-index: 1; `; const Title = styled.h2` font-size: 1.8rem; margin-bottom: 1.5rem; background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; letter-spacing: -0.02em; text-align: center; `; // New styled component for the JSON display container const JsonDisplayContainer = styled.div` height: 100%; max-height: 100%; overflow: auto; word-wrap: break-word; word-break: break-word; `; function JsonFormatter() { const { t } = useTranslation(); const [input, setInput] = useState(''); const [parsedJson, setParsedJson] = useState(null); const [isCopied, setIsCopied] = useState(false); const [isCompressed, setIsCompressed] = useState(false); const isLoading = usePageLoading(); useEffect(() => { try { if (input.trim()) { const parsed = JSON.parse(input); setParsedJson(parsed); } else { setParsedJson(null); } } catch (error) { setParsedJson(null); } }, [input]); const handleCopy = () => { if (parsedJson) { const formattedJson = isCompressed ? JSON.stringify(parsedJson) : JSON.stringify(parsedJson, null, 2); navigator.clipboard.writeText(formattedJson).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }); } }; const toggleCompression = () => { setIsCompressed(!isCompressed); }; return ( <> {isLoading && <LoadingOverlay />} <SEO title={t('tools.jsonFormatter.title')} description={t('tools.jsonFormatter.description')} /> <Container> <ContentWrapper> <Title>{t('tools.jsonFormatter.title')}</Title> <div className="flex flex-col lg:flex-row gap-4 lg:gap-6 h-[calc(100vh-220px)]"> <textarea className="w-full lg:w-5/12 p-4 text-sm font-mono border border-indigo-100 rounded-xl bg-white/80 backdrop-blur-sm focus:border-indigo-300 focus:ring-4 focus:ring-indigo-100 outline-none resize-none transition duration-300 overflow-auto" placeholder={t('tools.jsonFormatter.inputPlaceholder')} value={input} onChange={(e) => setInput(e.target.value)} /> <div className="w-full lg:w-7/12 relative border border-indigo-100 rounded-xl bg-white/80 backdrop-blur-sm p-4 flex flex-col h-full"> {parsedJson ? ( <> <JsonDisplayContainer className="font-mono text-sm leading-relaxed"> {isCompressed ? ( <pre className="m-0 whitespace-pre-wrap"> {JSON.stringify(parsedJson)} </pre> ) : ( <JsonView data={parsedJson} /> )} </JsonDisplayContainer> <div className="absolute top-4 right-4 flex gap-2 z-10"> <button onClick={toggleCompression} className={` flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200 ${isCompressed ? 'bg-indigo-100 text-indigo-700' : 'bg-white/50 hover:bg-indigo-50 text-gray-600 hover:text-indigo-600' } `} > {isCompressed ? ( <> <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> <path d="M4 9h16v2H4V9zm0 4h16v2H4v-2z"/> </svg> {t('tools.jsonFormatter.expand')} </> ) : ( <> <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> <path d="M19 13H5v-2h14v2z"/> </svg> {t('tools.jsonFormatter.compress')} </> )} </button> <button onClick={handleCopy} className={` flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-all duration-200 ${isCopied ? 'bg-green-100 text-green-700' : 'bg-white/50 hover:bg-indigo-50 text-gray-600 hover:text-indigo-600' } `} > {isCopied ? ( <> <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/> </svg> {t('tools.jsonFormatter.copied')} </> ) : ( <> <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> </svg> {t('tools.jsonFormatter.copy')} </> )} </button> </div> </> ) : ( <div className="p-4 text-gray-500"> {input.trim() ? t('tools.jsonFormatter.invalidJson') : t('tools.jsonFormatter.emptyInput')} </div> )} </div> </div> </ContentWrapper> </Container> </> ); } function JsonView({ data }) { if (data === null || data === undefined) return null; const [collapsedKeys, setCollapsedKeys] = useState(new Set()); const toggleCollapse = (key) => { const newCollapsedKeys = new Set(collapsedKeys); if (newCollapsedKeys.has(key)) { newCollapsedKeys.delete(key); } else { newCollapsedKeys.add(key); } setCollapsedKeys(newCollapsedKeys); }; const renderCollapsibleValue = (value, path = '') => { if (value === null) return <span className="text-indigo-400">null</span>; if (typeof value === 'boolean') return <span className="text-indigo-400">{value.toString()}</span>; if (typeof value === 'number') return <span className="text-emerald-500">{value}</span>; if (typeof value === 'string') return <span className="text-amber-500">"{value}"</span>; const isCollapsed = collapsedKeys.has(path); const hasChildren = Array.isArray(value) || (typeof value === 'object' && value !== null); if (Array.isArray(value)) { if (isCollapsed) { return ( <div className="inline-flex items-center gap-1"> <button onClick={() => toggleCollapse(path)} className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700" > <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> </svg> </button> <span className="text-gray-500">[...]</span> <span className="text-gray-400 text-sm ml-1">({value.length} items)</span> </div> ); } return ( <div className="ml-5"> <div className="inline-flex items-center gap-1"> <button onClick={() => toggleCollapse(path)} className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700" > <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> [ </div> {value.map((item, index) => ( <div key={index} className="ml-5"> {renderCollapsibleValue(item, `${path}[${index}]`)} {index < value.length - 1 ? ',' : ''} </div> ))} <div>]</div> </div> ); } if (typeof value === 'object') { const entries = Object.entries(value); if (isCollapsed) { return ( <div className="inline-flex items-center gap-1"> <button onClick={() => toggleCollapse(path)} className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700" > <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> </svg> </button> <span className="text-gray-500">{'{...}'}</span> <span className="text-gray-400 text-sm ml-1">({entries.length} properties)</span> </div> ); } return ( <div className="ml-5"> <div className="inline-flex items-center gap-1"> <button onClick={() => toggleCollapse(path)} className="w-4 h-4 inline-flex items-center justify-center text-gray-500 hover:text-gray-700" > <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> {'{'} </div> {entries.map(([key, val], index) => ( <div key={key} className="ml-5"> <span className="text-pink-500">"{key}"</span>: {renderCollapsibleValue(val, `${path}.${key}`)} {index < entries.length - 1 ? ',' : ''} </div> ))} <div>{'}'}</div> </div> ); } return value; }; return renderCollapsibleValue(data, 'root'); } export default JsonFormatter;