ca4bfc33fd
- 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>
199 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|