packages: move sharing logic from web to core; reuse in web

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I26cb6848615a5788f2196fc1675bc22d6a6a6964
This commit is contained in:
raf 2026-04-16 09:13:31 +03:00
commit 5482cd9df1
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
7 changed files with 481 additions and 1142 deletions

View file

@ -16,5 +16,8 @@
},
"devDependencies": {
"typescript": "^6.0.2"
},
"dependencies": {
"lzutf8": "^0.6.3"
}
}

View file

@ -1,2 +1,3 @@
export * from './types';
export * from './parser';
export * from './share';

View file

@ -0,0 +1,47 @@
import LZUTF8 from 'lzutf8';
export type ShareState =
| { type: 'analysis'; data: Record<string, unknown>; name: string }
| {
type: 'compare';
left: Record<string, unknown>;
right: Record<string, unknown>;
leftName: string;
rightName: string;
};
export function encodeShareUrl(state: ShareState): string {
const json = JSON.stringify(state);
const encoded = LZUTF8.compress(json, { outputEncoding: 'Base64' }) as string;
return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
export function decodeShareUrl(encoded: string): ShareState | null {
try {
let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
const padLength = (4 - (base64.length % 4)) % 4;
base64 += '='.repeat(padLength);
const json = LZUTF8.decompress(base64, {
inputEncoding: 'Base64',
outputEncoding: 'String',
}) as string | null;
if (!json) {
return null;
}
const state = JSON.parse(json) as ShareState;
if (state.type === 'analysis') {
if (!state.data || !state.name) {
return null;
}
} else if (state.type === 'compare') {
if (!state.left || !state.right || !state.leftName || !state.rightName) {
return null;
}
} else {
return null;
}
return state;
} catch {
return null;
}
}

View file

@ -11,8 +11,7 @@
},
"dependencies": {
"@ns/core": "workspace:*",
"@ns/ui-utils": "workspace:*",
"lzutf8": "^0.6.3"
"@ns/ui-utils": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.5.2",

View file

@ -65,7 +65,7 @@ const ComparisonView: Component<ComparisonViewProps> = props => {
setRightEntry(right);
}
}
if (props.initialLeftId !== null || props.initialRightId !== null) {
if (props.initialLeftId != null || props.initialRightId != null) {
props.onInitialSelectionUsed?.();
}
});

View file

@ -1,59 +1,21 @@
import { createSignal, Show, For, onMount, createEffect, lazy } from 'solid-js';
import { render } from 'solid-js/web';
import LZUTF8 from 'lzutf8';
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 } from '@ns/core';
import {
StatsData,
ComparisonEntry,
parseStats,
encodeShareUrl,
decodeShareUrl,
ShareState,
} from '@ns/core';
import './styles.css';
type ShareState =
| { type: 'analysis'; data: Record<string, unknown>; name: string }
| {
type: 'compare';
left: Record<string, unknown>;
right: Record<string, unknown>;
leftName: string;
rightName: string;
};
function encodeShareUrl(state: ShareState): string {
const json = JSON.stringify(state);
const encoded = LZUTF8.compress(json, { outputEncoding: 'Base64' }) as string;
return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function decodeShareUrl(encoded: string): ShareState | null {
try {
const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
const json = LZUTF8.decompress(base64, {
inputEncoding: 'Base64',
outputEncoding: 'String',
}) as string | null;
if (!json) {
return null;
}
const state = JSON.parse(json) as ShareState;
if (state.type === 'analysis') {
if (!state.data || !state.name) {
return null;
}
} else if (state.type === 'compare') {
if (!state.left || !state.right || !state.leftName || !state.rightName) {
return null;
}
} else {
return null;
}
return state;
} catch {
return null;
}
}
function debounce<T extends (...args: Parameters<T>) => ReturnType<T>>(
fn: T,
delay: number,
@ -268,10 +230,15 @@ function App() {
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);
});
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) => {
@ -366,10 +333,15 @@ function App() {
};
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);
});
navigator.clipboard
.writeText(url)
.then(() => {
setShowShareToast(true);
setTimeout(() => setShowShareToast(false), 2000);
})
.catch(err => {
console.error('Failed to copy share URL:', err);
});
};
return (

1487
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff