Files
testing-tool/src/pages/JsonTools/index.tsx
T
雨霖铃 ca4bfc33fd refactor: clean up unused code and simplify imports
- Remove unused imports and variables across 35 files
- Simplify component logic and remove dead code
- Clean up test files by removing unnecessary setup
- Streamline CI workflow configuration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 20:57:08 +08:00

199 lines
7.3 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useI18n } from '@/utils/chromeI18n';
import JsonDiffInput from './JsonDiffInput';
import DiffResult from './DiffResult';
import DiffNavigator from './DiffNavigator';
import JsonFormatSection from './JsonFormatSection';
import type { ConvertFunction } from './JsonConvertSection';
import JsonConvertSection from './JsonConvertSection';
import { diffJson } from './diffEngine';
import { jsonToYaml } from '@/utils/jsonToYaml';
import { jsonToToml } from '@/utils/jsonToToml';
import { minifyJson } from '@/utils/jsonFormatter';
import { useStorageState } from '@/utils/useStorageState';
import type { JsonToolsPageMode } from '@/types/storage';
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import type { ViewMode } from './types';
interface ParseState {
value: unknown;
error: string | null;
}
const tryParse = (raw: string, invalidMsg: string): ParseState => {
const trimmed = raw.trim();
if (!trimmed) return { value: undefined, error: null };
try {
return { value: JSON.parse(trimmed), error: null };
} catch {
return { value: undefined, error: invalidMsg };
}
};
const VALID_PAGE_MODES: readonly JsonToolsPageMode[] = ['diff', 'format', 'yaml', 'toml', 'minify'];
const isValidPageMode = (val: unknown): val is JsonToolsPageMode =>
typeof val === 'string' && (VALID_PAGE_MODES as readonly string[]).includes(val);
type PageMode = JsonToolsPageMode;
export default function Index() {
const { t } = useI18n(['jsonDiff', 'jsonFormat']);
const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode);
// Debounce input
const [leftInput, setLeftInput] = useState('');
const [rightInput, setRightInput] = useState('');
// Debounced values
const [debouncedLeft, setDebouncedLeft] = useState('');
const [debouncedRight, setDebouncedRight] = useState('');
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedLeft(leftInput);
setDebouncedRight(rightInput);
}, 250);
return () => clearTimeout(handle);
}, [leftInput, rightInput]);
// Parse debounced inputs
const parseState = useMemo(() => {
const invalidMsg = t('jsonDiff:invalidJson');
return {
left: tryParse(debouncedLeft, invalidMsg),
right: tryParse(debouncedRight, invalidMsg),
};
}, [debouncedLeft, debouncedRight, t]);
const leftError = parseState.left.error;
const rightError = parseState.right.error;
const [viewMode, setViewMode] = useState<ViewMode>('sideBySide');
const [currentDiffIndex, setCurrentDiffIndex] = useState(0);
// Real-time diff computation
const diffResult = useMemo(() => {
const { left, right } = parseState;
if (left.error || right.error || debouncedLeft.trim() === '' || debouncedRight.trim() === '') {
return null;
}
return diffJson(left.value, right.value);
}, [parseState, debouncedLeft, debouncedRight]);
const total = diffResult?.diffPaths.length ?? 0;
const handlePrev = useCallback(() => {
if (total === 0) return;
setCurrentDiffIndex((idx) => (idx - 1 + total) % total);
}, [total]);
const handleNext = useCallback(() => {
if (total === 0) return;
setCurrentDiffIndex((idx) => (idx + 1) % total);
}, [total]);
const activePath = diffResult && total > 0 ? diffResult.diffPaths[currentDiffIndex] : undefined;
const yamlConvert: ConvertFunction = useCallback((text: string) => {
const r = jsonToYaml(text);
return { output: r.output, originalBytes: r.originalBytes, outputBytes: r.outputBytes };
}, []);
const tomlConvert: ConvertFunction = useCallback((text: string) => {
const r = jsonToToml(text);
return { output: r.output, originalBytes: r.originalBytes, outputBytes: r.outputBytes };
}, []);
const minifyConvert: ConvertFunction = useCallback((text: string) => {
const r = minifyJson(text);
return { output: r.minified, originalBytes: r.originalBytes, outputBytes: r.minifiedBytes };
}, []);
return (
<div className="p-4 w-full flex flex-col space-y-4 min-h-[500px] select-none">
<SwitchButtonGroup
value={pageMode}
onChange={(v: PageMode) => setPageMode(v)}
options={[
{ value: 'diff', label: t('jsonFormat:diffMode') },
{ value: 'format', label: t('jsonFormat:formatMode') },
{ value: 'yaml', label: t('jsonFormat:yamlMode') },
{ value: 'toml', label: t('jsonFormat:tomlMode') },
{ value: 'minify', label: t('jsonFormat:minifyMode') },
]}
size="small"
className="w-full sm:w-auto"
/>
{pageMode === 'diff' ? (
<div className="flex flex-col space-y-4">
<div className="flex h-10 items-center justify-between px-1.5 bg-secondary/40 rounded-xl border border-border/60">
<SwitchButtonGroup
value={viewMode}
onChange={(v: ViewMode) => setViewMode(v)}
options={[
{ value: 'sideBySide', label: t('jsonDiff:sideBySideMode') },
{ value: 'unified', label: t('jsonDiff:unifiedMode') },
]}
size="small"
/>
</div>
<div className="flex flex-col md:flex-row gap-4 w-full items-stretch">
<JsonDiffInput
label={t('jsonDiff:leftLabel')}
placeholder={t('jsonDiff:leftPlaceholder')}
value={leftInput}
onChange={(val) => {
setLeftInput(val);
setCurrentDiffIndex(0);
}}
error={leftError}
minRows={9}
/>
<JsonDiffInput
label={t('jsonDiff:rightLabel')}
placeholder={t('jsonDiff:rightPlaceholder')}
value={rightInput}
onChange={(val) => {
setRightInput(val);
setCurrentDiffIndex(0);
}}
error={rightError}
minRows={9}
/>
</div>
{diffResult ? (
<div className="flex flex-col space-y-3.5 w-full pt-1">
<div className="flex justify-center w-full">
<DiffNavigator
total={total}
currentIndex={currentDiffIndex}
onPrev={handlePrev}
onNext={handleNext}
/>
</div>
<DiffResult result={diffResult} viewMode={viewMode} activePath={activePath} />
</div>
) : (
<div className="p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center min-h-[140px]">
<p className="text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[260px] leading-relaxed">
{leftError || rightError ? t('jsonDiff:fixErrorHint') : t('jsonDiff:emptyHint')}
</p>
</div>
)}
</div>
) : pageMode === 'format' ? (
<JsonFormatSection />
) : pageMode === 'yaml' ? (
<JsonConvertSection translationPrefix="yaml" convertFunction={yamlConvert} />
) : pageMode === 'toml' ? (
<JsonConvertSection translationPrefix="toml" convertFunction={tomlConvert} />
) : (
<JsonConvertSection translationPrefix="minify" convertFunction={minifyConvert} />
)}
</div>
);
}