import { createSignal, Show, For, onMount, createEffect, lazy } from 'solid-js'; import { render } from 'solid-js/web'; import SaveIcon from 'lucide-solid/icons/save'; import UploadIcon from 'lucide-solid/icons/upload'; import Trash2Icon from 'lucide-solid/icons/trash-2'; import XIcon from 'lucide-solid/icons/x'; import ShareIcon from 'lucide-solid/icons/link-2'; import FileUpload from './components/FileUpload'; import { StatsData, ComparisonEntry, parseStats, encodeShareUrl, decodeShareUrl, ShareState, } from '@ns/core'; import './styles.css'; function debounce) => ReturnType>( fn: T, delay: number, ): (...args: Parameters) => void { let timeoutId: ReturnType; return (...args: Parameters) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } const Analysis = lazy(() => import('./components/Analysis')); const ComparisonView = lazy(() => import('./components/ComparisonView')); function App() { const [currentStats, setCurrentStats] = createSignal(null); const [currentRaw, setCurrentRaw] = createSignal | null>(null); const [snapshots, setSnapshots] = createSignal([]); const [view, setView] = createSignal<'analysis' | 'compare'>('analysis'); const [snapshotName, setSnapshotName] = createSignal(''); const [showSaveDialog, setShowSaveDialog] = createSignal(false); const [showManageSnapshots, setShowManageSnapshots] = createSignal(false); const [precision, setPrecision] = createSignal(2); const [pasteMode, setPasteMode] = createSignal<'advance' | 'replace'>('advance'); const [isLoading, setIsLoading] = createSignal(true); const [showOverrideModal, setShowOverrideModal] = createSignal(false); const [pendingOverrideText, setPendingOverrideText] = createSignal(''); const [showShareToast, setShowShareToast] = createSignal(false); const [initialLeftId, setInitialLeftId] = createSignal(null); const [initialRightId, setInitialRightId] = createSignal(null); const STORAGE_KEY = 'ns-data'; // Load from localStorage on mount onMount(() => { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (Array.isArray(parsed.snapshots)) { setSnapshots( parsed.snapshots.map((s: ComparisonEntry) => ({ ...s, raw: s.raw || {}, })), ); } if (parsed.currentStats) { setCurrentStats(parsed.currentStats); } if (parsed.currentRaw) { setCurrentRaw(parsed.currentRaw); } if (parsed.view) { setView(parsed.view); } if (typeof parsed.precision === 'number' && parsed.precision >= 0) { setPrecision(parsed.precision); } if (parsed.pasteMode === 'advance' || parsed.pasteMode === 'replace') { setPasteMode(parsed.pasteMode); } } } catch (e) { console.warn('Failed to load saved data:', e); } const params = new URLSearchParams(window.location.search); const shareParam = params.get('share'); if (shareParam) { const shareState = decodeShareUrl(shareParam); if (shareState) { try { if (shareState.type === 'analysis') { const data = parseStats(shareState.data); setCurrentStats(data); setCurrentRaw(shareState.data); setSnapshotName(shareState.name); setView('analysis'); window.history.replaceState({}, '', window.location.pathname); } else if (shareState.type === 'compare') { const leftData = parseStats(shareState.left); const rightData = parseStats(shareState.right); const leftId = Date.now(); const rightId = Date.now() + 1; const leftEntry: ComparisonEntry = { id: leftId, name: shareState.leftName, data: leftData, raw: shareState.left, timestamp: new Date(), }; const rightEntry: ComparisonEntry = { id: rightId, name: shareState.rightName, data: rightData, raw: shareState.right, timestamp: new Date(), }; setSnapshots([leftEntry, rightEntry]); setInitialLeftId(leftId); setInitialRightId(rightId); setView('compare'); window.history.replaceState({}, '', window.location.pathname); } } catch (e) { console.warn('Failed to load shared data:', e); } } } setIsLoading(false); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (showSaveDialog()) setShowSaveDialog(false); if (showManageSnapshots()) setShowManageSnapshots(false); } }; window.addEventListener('keydown', handleKeyDown); const handleAnalysisPaste = (e: ClipboardEvent) => { const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } const text = e.clipboardData?.getData('text'); if (!text) return; try { JSON.parse(text); } catch { return; } if (view() === 'analysis' && currentStats()) { setPendingOverrideText(text); setShowOverrideModal(true); } }; document.addEventListener('paste', handleAnalysisPaste); return () => { window.removeEventListener('keydown', handleKeyDown); document.removeEventListener('paste', handleAnalysisPaste); }; }); // Debounced save to localStorage const saveToStorage = debounce( ( stats: StatsData | null, raw: Record | null, snaps: ComparisonEntry[], v: 'analysis' | 'compare', prec: number, pm: 'advance' | 'replace', ) => { try { localStorage.setItem( STORAGE_KEY, JSON.stringify({ snapshots: snaps, currentStats: stats, currentRaw: raw, view: v, precision: prec, pasteMode: pm, }), ); } catch (e) { console.warn('Failed to save data:', e); } }, 500, ); // Save to localStorage on any change createEffect(() => { const stats = currentStats(); const raw = currentRaw(); const snaps = snapshots(); const v = view(); const prec = precision(); const pm = pasteMode(); if (isLoading()) return; saveToStorage(stats, raw, snaps, v, prec, pm); }); const saveSnapshot = () => { const stats = currentStats(); const raw = currentRaw(); if (!stats || !raw) return; const name = snapshotName().trim() || `Snapshot ${snapshots().length + 1}`; const entry: ComparisonEntry = { id: Date.now(), name, data: stats, raw, timestamp: new Date(), }; setSnapshots(prev => [...prev, entry]); setSnapshotName(''); setShowSaveDialog(false); }; const shareAnalysis = () => { const stats = currentStats(); const raw = currentRaw(); if (!stats || !raw) return; const name = snapshotName().trim() || `Analysis ${Date.now()}`; const state: ShareState = { type: 'analysis', data: raw, name }; const encoded = encodeShareUrl(state); const url = `${window.location.origin}${window.location.pathname}?share=${encoded}`; navigator.clipboard .writeText(url) .then(() => { setShowShareToast(true); setTimeout(() => setShowShareToast(false), 2000); }) .catch(err => { console.error('Failed to copy share URL:', err); }); }; const deleteSnapshot = (id: number) => { setSnapshots(prev => prev.filter(e => e.id !== id)); }; const clearAllSnapshots = () => { if (confirm('Delete all saved snapshots?')) { setSnapshots([]); } }; const loadSnapshot = (entry: ComparisonEntry) => { setCurrentStats(entry.data); setView('analysis'); setShowManageSnapshots(false); }; const handleFileLoad = (data: StatsData, raw: Record) => { setCurrentStats(data); setCurrentRaw(raw); setView('analysis'); }; const loadFromText = (text: string) => { try { const raw = JSON.parse(text); const data = parseStats(raw); setCurrentStats(data); setCurrentRaw(raw); } catch (e) { console.error('Failed to parse stats:', e); } }; const handlePasteStats = (text: string, name: string): ComparisonEntry | null => { let raw: Record; let data: StatsData; try { raw = JSON.parse(text); data = parseStats(raw); } catch (e) { console.error('Failed to parse pasted stats:', e); return null; } const entry: ComparisonEntry = { id: Date.now(), name: name.trim() || `Snapshot ${snapshots().length + 1}`, data, raw, timestamp: new Date(), }; setSnapshots(prev => [...prev, entry]); return entry; }; const handleCompareFileLoad = (data: StatsData, raw: Record) => { const entry: ComparisonEntry = { id: Date.now(), name: `Snapshot ${snapshots().length + 1}`, data, raw, timestamp: new Date(), }; setSnapshots(prev => [...prev, entry]); }; const handleCompareTextLoad = (text: string) => { try { const raw = JSON.parse(text); const data = parseStats(raw); const entry: ComparisonEntry = { id: Date.now(), name: `Snapshot ${snapshots().length + 1}`, data, raw, timestamp: new Date(), }; setSnapshots(prev => [...prev, entry]); } catch (e) { console.error('Failed to parse stats:', e); } }; const handleGenerateShareUrl = (left: ComparisonEntry, right: ComparisonEntry) => { const state: ShareState = { type: 'compare', left: left.raw, right: right.raw, leftName: left.name, rightName: right.name, }; const encoded = encodeShareUrl(state); const url = `${window.location.origin}${window.location.pathname}?share=${encoded}`; navigator.clipboard .writeText(url) .then(() => { setShowShareToast(true); setTimeout(() => setShowShareToast(false), 2000); }) .catch(err => { console.error('Failed to copy share URL:', err); }); }; return (

NS

Loading saved data...
setCurrentStats(entry.data)} onDelete={deleteSnapshot} precision={precision()} pasteMode={pasteMode()} onPasteModeChange={setPasteMode} onPasteStats={handlePasteStats} onFileLoad={handleCompareFileLoad} onTextLoad={handleCompareTextLoad} onGenerateShareUrl={handleGenerateShareUrl} initialLeftId={initialLeftId()} initialRightId={initialRightId()} onInitialSelectionUsed={() => { setInitialLeftId(null); setInitialRightId(null); }} />
Share URL copied to clipboard!
); } render(() => , document.getElementById('root')!);