refactor: only include template in share url if it's custom

This commit is contained in:
Lewis Wynne 2026-03-23 19:26:45 +00:00
parent bb429d8195
commit be11c0e57a
6 changed files with 249 additions and 32 deletions

View file

@ -1,13 +1,26 @@
<script lang="ts">
import { Sun, Moon } from 'lucide-svelte';
import { Sun, Moon, Share2, Check } from 'lucide-svelte';
import { theme } from '$lib/theme.svelte';
import { roster } from '$lib/state.svelte';
import { presets } from '$lib/presets';
import { encodeCharacterURL } from '$lib/sharing';
import CharacterSwitcher from './CharacterSwitcher.svelte';
let copied = $state(false);
async function createCharacter() {
await roster.create(presets[0]);
}
async function share() {
const char = roster.active;
if (!char) return;
const encoded = encodeCharacterURL(char);
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
await navigator.clipboard.writeText(url);
copied = true;
setTimeout(() => { copied = false; }, 2000);
}
</script>
<header class="sticky top-0 z-20 flex items-center gap-3 px-4 py-3 border-b" style="border-color: var(--border); background: var(--bg-card);">
@ -22,6 +35,16 @@
{#if roster.saveStatus === 'saving'}Saving...{:else if roster.saveStatus === 'saved'}Saved{/if}
</span>
{#if roster.active}
<button onclick={share} class="p-1 rounded hover:opacity-80" title="Copy share link">
{#if copied}
<Check size={18} />
{:else}
<Share2 size={18} />
{/if}
</button>
{/if}
<button
onclick={createCharacter}
class="px-3 py-1 rounded text-sm border hover:opacity-80"

View file

@ -0,0 +1,126 @@
<script lang="ts">
import { decodeCharacterURL, decodeTemplateURL } from '$lib/sharing';
import { generateRecord } from '$lib/output';
import { species } from '$lib/data';
import { roster } from '$lib/state.svelte';
import { slugify } from '$lib/utils/slugify';
import OutputTab from './OutputTab.svelte';
let { encoded, onClose }: { encoded: string; onClose: () => void } = $props();
let error = $state('');
let type = $state<'character' | 'template' | null>(null);
let charData = $state<{ template: any; data: Record<string, unknown> } | null>(null);
let tmplData = $state<any>(null);
let activeTab = $state('');
$effect(() => {
try {
if (encoded.startsWith('c1.')) {
type = 'character';
charData = decodeCharacterURL(encoded);
} else if (encoded.startsWith('t1.')) {
type = 'template';
tmplData = decodeTemplateURL(encoded);
} else {
error = 'Unrecognized share link format.';
}
} catch {
error = 'Failed to decode share link.';
}
});
let tabs = $derived(
charData?.template.records.filter((r: any) => r.type !== 'public') ?? []
);
$effect(() => {
if (tabs.length && !tabs.some((t: any) => t.type === activeTab)) {
activeTab = tabs[0].type;
}
});
let output = $derived(
charData && activeTab
? generateRecord(charData.template, charData.data, activeTab, species)
: ''
);
function charName(): string {
if (!charData) return 'Unknown';
return (charData.data[slugify('Name')] as string) || 'Unnamed Character';
}
async function importCharacter() {
if (!charData) return;
const char = await roster.create(charData.template);
Object.assign(char.data, charData.data);
roster.scheduleSave(char);
onClose();
}
async function importTemplate() {
if (!tmplData) return;
await roster.create(tmplData);
onClose();
}
</script>
<div class="min-h-screen flex flex-col items-center p-4">
<div class="w-full max-w-2xl">
{#if error}
<div class="rounded border p-6 text-center" style="border-color: var(--border); background: var(--bg-card);">
<p class="mb-4">{error}</p>
<button onclick={onClose} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
Dismiss
</button>
</div>
{:else if type === 'character' && charData}
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="font-semibold">{charName()}</h2>
<p class="text-sm" style="color: var(--text-muted);">Shared character — {charData.template.name} template</p>
</div>
<div class="flex gap-2">
<button onclick={onClose} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
Cancel
</button>
<button onclick={importCharacter} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
Import
</button>
</div>
</div>
<div class="rounded border" style="border-color: var(--border); background: var(--bg-card);">
<div class="flex border-b" style="border-color: var(--border);">
{#each tabs as tab}
<button
onclick={() => { activeTab = tab.type; }}
class="px-4 py-2 text-sm capitalize"
style={activeTab === tab.type
? `color: var(--accent); border-bottom: 2px solid var(--accent);`
: `color: var(--text-muted); border-bottom: 2px solid transparent;`}
>
{tab.type}
</button>
{/each}
</div>
<OutputTab {output} />
</div>
{:else if type === 'template' && tmplData}
<div class="rounded border p-6 text-center" style="border-color: var(--border); background: var(--bg-card);">
<h2 class="font-semibold mb-2">Shared Template: {tmplData.name}</h2>
<p class="text-sm mb-4" style="color: var(--text-muted);">{tmplData.records.length} records, {tmplData.records.reduce((n: number, r: any) => n + r.fields.length, 0)} fields</p>
<div class="flex gap-2 justify-center">
<button onclick={onClose} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
Cancel
</button>
<button onclick={importTemplate} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
Create Character
</button>
</div>
</div>
{/if}
</div>
</div>

View file

@ -0,0 +1,25 @@
<script lang="ts">
let { onClose, children }: { onClose: () => void; children: any } = $props();
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center"
onmousedown={onClose}
>
<div class="absolute inset-0 bg-black/50"></div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="relative rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
style="background: var(--bg-card); color: var(--text);"
onmousedown={(e) => e.stopPropagation()}
>
{@render children()}
</div>
</div>