From be11c0e57a3fdd324a73eba81240613f07e43e36 Mon Sep 17 00:00:00 2001 From: lew Date: Mon, 23 Mar 2026 19:26:45 +0000 Subject: [PATCH] refactor: only include template in share url if it's custom --- src/lib/components/Header.svelte | 25 ++++- src/lib/components/ImportModal.svelte | 126 ++++++++++++++++++++++++++ src/lib/components/Modal.svelte | 25 +++++ src/lib/sharing.test.ts | 64 +++++++------ src/lib/sharing.ts | 21 ++++- src/routes/+page.svelte | 20 +++- 6 files changed, 249 insertions(+), 32 deletions(-) create mode 100644 src/lib/components/ImportModal.svelte create mode 100644 src/lib/components/Modal.svelte diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 4178f74..877af38 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,13 +1,26 @@
@@ -22,6 +35,16 @@ {#if roster.saveStatus === 'saving'}Saving...{:else if roster.saveStatus === 'saved'}Saved{/if} + {#if roster.active} + + {/if} + + + {:else if type === 'character' && charData} +
+
+

{charName()}

+

Shared character — {charData.template.name} template

+
+
+ + +
+
+ +
+
+ {#each tabs as tab} + + {/each} +
+ +
+ {:else if type === 'template' && tmplData} +
+

Shared Template: {tmplData.name}

+

{tmplData.records.length} records, {tmplData.records.reduce((n: number, r: any) => n + r.fields.length, 0)} fields

+
+ + +
+
+ {/if} + + diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte new file mode 100644 index 0000000..ed11d8f --- /dev/null +++ b/src/lib/components/Modal.svelte @@ -0,0 +1,25 @@ + + + + + +
+
+ +
e.stopPropagation()} + > + {@render children()} +
+
diff --git a/src/lib/sharing.test.ts b/src/lib/sharing.test.ts index 02f151c..d4eae67 100644 --- a/src/lib/sharing.test.ts +++ b/src/lib/sharing.test.ts @@ -5,12 +5,25 @@ import { encodeTemplateURL, decodeTemplateURL } from './sharing'; +import { presets } from './presets'; import type { Character, Template } from './types'; -const testTemplate: Template = { - id: 'preset:standard', - name: 'Standard', - description: 'The standard record format.', +const standardPreset = presets.find((p) => p.id === 'preset:standard')!; + +const testCharacter: Character = { + id: 'abc-123', + template: standardPreset, + data: { + name: 'Yury Zakharov', + species: 'human', + 'employment-history': 'Shaft Miner' + } +}; + +const customTemplate: Template = { + id: 'custom:test', + name: 'Custom', + description: 'A custom template.', schemaVersion: 1, records: [ { @@ -23,35 +36,34 @@ const testTemplate: Template = { ] }; -const testCharacter: Character = { - id: 'abc-123', - template: testTemplate, - data: { - name: 'Yury Zakharov', - species: 'human', - 'employment-history': 'Shaft Miner' - } -}; - describe('character URL encoding', () => { - it('round-trips character data', () => { + it('round-trips preset character data', () => { const encoded = encodeCharacterURL(testCharacter); const decoded = decodeCharacterURL(encoded); expect(decoded.data).toEqual(testCharacter.data); expect(decoded.template.name).toBe('Standard'); }); + it('uses short encoding for preset templates', () => { + const encoded = encodeCharacterURL(testCharacter); + const customChar = { ...testCharacter, template: customTemplate }; + const customEncoded = encodeCharacterURL(customChar); + expect(encoded.length).toBeLessThan(customEncoded.length); + }); + + it('round-trips custom template character', () => { + const char: Character = { ...testCharacter, template: customTemplate }; + const encoded = encodeCharacterURL(char); + const decoded = decodeCharacterURL(encoded); + expect(decoded.data).toEqual(testCharacter.data); + expect(decoded.template.name).toBe('Custom'); + }); + it('starts with c1. prefix', () => { const encoded = encodeCharacterURL(testCharacter); expect(encoded.startsWith('c1.')).toBe(true); }); - it('strips id', () => { - const encoded = encodeCharacterURL(testCharacter); - const decoded = decodeCharacterURL(encoded); - expect(decoded).not.toHaveProperty('id'); - }); - it('prunes empty values from data', () => { const char: Character = { ...testCharacter, @@ -65,19 +77,19 @@ describe('character URL encoding', () => { describe('template URL encoding', () => { it('round-trips template structure', () => { - const encoded = encodeTemplateURL(testTemplate); + const encoded = encodeTemplateURL(customTemplate); const decoded = decodeTemplateURL(encoded); - expect(decoded.name).toBe('Standard'); - expect(decoded.records).toEqual(testTemplate.records); + expect(decoded.name).toBe('Custom'); + expect(decoded.records).toEqual(customTemplate.records); }); it('starts with t1. prefix', () => { - const encoded = encodeTemplateURL(testTemplate); + const encoded = encodeTemplateURL(customTemplate); expect(encoded.startsWith('t1.')).toBe(true); }); it('strips id', () => { - const encoded = encodeTemplateURL(testTemplate); + const encoded = encodeTemplateURL(customTemplate); const decoded = decodeTemplateURL(encoded); expect(decoded).not.toHaveProperty('id'); }); diff --git a/src/lib/sharing.ts b/src/lib/sharing.ts index 0726f10..160f75c 100644 --- a/src/lib/sharing.ts +++ b/src/lib/sharing.ts @@ -1,5 +1,6 @@ import pako from 'pako'; import type { Character, Template } from './types'; +import { presets } from './presets'; function toBase64url(bytes: Uint8Array): string { let binary = ''; @@ -26,20 +27,32 @@ function pruneEmpty(data: Record): Record { } export function encodeCharacterURL(char: Character): string { - const payload = { - template: stripId(char.template), + const isPreset = char.template.id.startsWith('preset:'); + const payload: any = { data: pruneEmpty(char.data) }; + if (isPreset) { + payload.templateId = char.template.id; + } else { + payload.template = stripId(char.template); + } const json = JSON.stringify(payload); const compressed = pako.deflate(new TextEncoder().encode(json)); return 'c1.' + toBase64url(compressed); } -export function decodeCharacterURL(encoded: string): { template: Omit; data: Record } { +export function decodeCharacterURL(encoded: string): { template: Template | Omit; data: Record } { if (!encoded.startsWith('c1.')) throw new Error('Invalid character URL prefix'); const bytes = fromBase64url(encoded.slice(3)); const json = new TextDecoder().decode(pako.inflate(bytes)); - return JSON.parse(json); + const payload = JSON.parse(json); + + if (payload.templateId) { + const preset = presets.find((p) => p.id === payload.templateId); + if (!preset) throw new Error(`Unknown template: ${payload.templateId}`); + return { template: preset, data: payload.data }; + } + return { template: payload.template, data: payload.data }; } export function encodeTemplateURL(template: Template): string { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 58b1204..72c5143 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,15 +1,33 @@
- {#if roster.active} + {#if importData} + + {:else if roster.active} {@const char = roster.active}