added sharing and subscriptions

This commit is contained in:
2026-06-17 23:27:37 +02:00
parent 148e441425
commit 8a614cb1d1
16 changed files with 1019 additions and 26 deletions
+102 -12
View File
@@ -27,6 +27,55 @@
let targetPrice = $state('');
let busy = $state(false);
let formError = $state('');
let showDetails = $state(false);
// sharing
let sharing = $state(false);
let copied = $state(false);
const shareUrl = $derived(
list?.share_token ? `${page.url.origin}/shared/${list.share_token}` : ''
);
async function share() {
if (!list) return;
sharing = true;
formError = '';
try {
const updated = await listsApi.share(list.id);
list = updated;
lists.replace(updated);
} catch (err) {
formError = err instanceof ApiError ? err.message : 'failed to share';
} finally {
sharing = false;
}
}
async function unshare() {
if (!list || !confirm('revoke the share link? anyone holding it loses access.')) return;
sharing = true;
formError = '';
try {
const updated = await listsApi.unshare(list.id);
list = updated;
lists.replace(updated);
} catch (err) {
formError = err instanceof ApiError ? err.message : 'failed to unshare';
} finally {
sharing = false;
}
}
async function copyShare() {
if (!shareUrl) return;
try {
await navigator.clipboard.writeText(shareUrl);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {
/* clipboard blocked — the field is selectable as a fallback */
}
}
// inline edit
let editingId = $state<string | null>(null);
@@ -242,24 +291,65 @@
<a href="/lists" class="label hover:text-iris">← all lists</a>
<div class="mt-2 flex items-start gap-3">
<span class="text-4xl leading-none">{list?.emoji ?? '✦'}</span>
<div>
<div class="min-w-0 flex-1">
<h1 class="font-display text-4xl font-bold">{list?.name ?? '…'}</h1>
{#if list?.description}<p class="gospel mt-1 text-lg">{list.description}</p>{/if}
</div>
{#if list}
{#if list.share_token}
<button
class="tag shrink-0 border-mint text-mint"
title="this list is shared — manage below"
onclick={() => document.getElementById('share-box')?.scrollIntoView({ behavior: 'smooth' })}
>
◈ shared
</button>
{:else}
<button class="tag shrink-0 border-smoke text-mute hover:text-iris" disabled={sharing} onclick={share}>
{sharing ? '…' : '◈ share'}
</button>
{/if}
{/if}
</div>
{#if list?.share_token}
<div id="share-box" class="panel mt-4 flex flex-wrap items-center gap-2 p-3 text-sm">
<span class="label shrink-0">public link</span>
<input class="field flex-1 text-xs" readonly value={shareUrl} onclick={(e) => e.currentTarget.select()} />
<button class="rounded border border-smoke px-3 py-1.5 text-xs text-mute transition hover:border-iris hover:text-iris" onclick={copyShare}>
{copied ? '✓ copied' : 'copy'}
</button>
<a href={shareUrl} target="_blank" rel="noopener noreferrer" class="rounded border border-smoke px-3 py-1.5 text-xs text-mute transition hover:border-iris hover:text-iris">open ↗</a>
<button class="rounded border border-smoke px-3 py-1.5 text-xs text-mute transition hover:border-rose hover:text-rose" disabled={sharing} onclick={unshare}>
{sharing ? '…' : 'unshare'}
</button>
</div>
{/if}
</div>
<!-- Add item -->
<form class="panel panel-acid space-y-4 p-6" onsubmit={addItem}>
<p class="label">add an item</p>
<input class="field" bind:value={title} maxlength="200" placeholder="what do you want?" />
<div class="grid gap-4 sm:grid-cols-[1fr_8rem]">
<input class="field" bind:value={url} placeholder="product link (we'll track the price)" />
<input class="field" bind:value={targetPrice} inputmode="decimal" placeholder="target price" />
<!-- Add item — compact quick-add; tracking details on demand -->
<form class="panel space-y-3 p-4" onsubmit={addItem}>
<div class="flex gap-2">
<input class="field" bind:value={title} maxlength="200" placeholder="add an item — what do you want?" />
<button class="btn btn-acid shrink-0" disabled={busy || !title.trim()}>{busy ? '…' : 'add +'}</button>
</div>
<input class="field" bind:value={note} maxlength="1000" placeholder="note to self (optional)" />
<button
type="button"
class="label transition hover:text-iris"
onclick={() => (showDetails = !showDetails)}
>
{showDetails ? ' fewer' : '+ link, target price & note'}
</button>
{#if showDetails}
<div class="space-y-3 border-t border-smoke pt-3">
<div class="grid gap-3 sm:grid-cols-[1fr_8rem]">
<input class="field" bind:value={url} placeholder="product link (we'll track the price)" />
<input class="field" bind:value={targetPrice} inputmode="decimal" placeholder="target price" />
</div>
<input class="field" bind:value={note} maxlength="1000" placeholder="note to self (optional)" />
</div>
{/if}
{#if formError}<p class="border-2 border-rose bg-rose/10 px-3 py-2 text-sm text-rose">{formError}</p>{/if}
<button class="btn btn-acid" disabled={busy}>{busy ? 'adding…' : 'add item +'}</button>
</form>
{#if !loaded}
@@ -298,8 +388,8 @@
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-mute">
{#if money(item.current_price, item.currency)}
<span class:text-mint={onSale(item)} class:text-ink={!onSale(item)}>
now {money(item.current_price, item.currency)}
<span class="text-sm font-bold" class:text-mint={onSale(item)} class:text-ink={!onSale(item)}>
{money(item.current_price, item.currency)}
</span>
{/if}
{#if item.target_price != null}