updated wording

This commit is contained in:
2026-06-17 16:39:43 +02:00
parent 8b1b9cedc2
commit 7a90ced98e
14 changed files with 257 additions and 79 deletions
+113 -21
View File
@@ -27,6 +27,12 @@
let busy = $state(false);
let formError = $state('');
// inline edit
let editingId = $state<string | null>(null);
let edit = $state({ title: '', url: '', note: '', target: '', currency: '' });
let editError = $state('');
let editBusy = $state(false);
// tracking
let refetchingId = $state<string | null>(null);
let historyFor = $state<string | null>(null);
@@ -101,8 +107,57 @@
}
}
function startEdit(item: Item) {
editingId = item.id;
editError = '';
edit = {
title: item.title,
url: item.url ?? '',
note: item.note ?? '',
target: item.target_price != null ? String(item.target_price) : '',
currency: item.currency ?? ''
};
}
function cancelEdit() {
editingId = null;
editError = '';
}
async function saveEdit(item: Item) {
if (!edit.title.trim()) {
editError = 'title is required';
return;
}
editBusy = true;
editError = '';
try {
const t = edit.target.trim();
const tp = t ? Number(t) : null; // null clears the target → notify on any drop
if (t && !Number.isFinite(tp as number)) {
editError = 'target must be a number';
return;
}
const cur = edit.currency.trim().toUpperCase();
const updated = await listsApi.updateItem(item.id, {
title: edit.title.trim(),
url: edit.url.trim() || null,
note: edit.note.trim() || null,
target_price: tp,
...(cur ? { currency: cur } : {})
});
const i = items.findIndex((x) => x.id === item.id);
if (i >= 0) items[i] = updated;
editingId = null;
} catch (err) {
editError = err instanceof ApiError ? err.message : 'failed to save';
} finally {
editBusy = false;
}
}
async function removeItem(item: Item) {
if (!confirm(`cast out${item.title}”?`)) return;
if (!confirm(`remove${item.title}”?`)) return;
try {
await listsApi.removeItem(item.id);
items = items.filter((x) => x.id !== item.id);
@@ -120,7 +175,7 @@
if (i >= 0) items[i] = updated;
if (historyFor === item.id) history = await listsApi.history(item.id);
} catch (err) {
formError = err instanceof ApiError ? err.message : 'failed to keep vigil';
formError = err instanceof ApiError ? err.message : 'failed to check price';
} finally {
refetchingId = null;
}
@@ -137,7 +192,7 @@
try {
history = await listsApi.history(item.id);
} catch (err) {
formError = err instanceof ApiError ? err.message : 'failed to read the chronicle';
formError = err instanceof ApiError ? err.message : 'failed to load history';
} finally {
historyLoading = false;
}
@@ -167,9 +222,9 @@
renounced: 'border-smoke text-mute'
};
const STATUS_LABEL: Record<ItemStatus, string> = {
coveted: 'coveted',
acquired: 'acquired',
renounced: 'renounced'
coveted: 'want',
acquired: 'bought',
renounced: 'skip'
};
function money(v: number | null, cur: string | null) {
@@ -193,27 +248,27 @@
</div>
</div>
<!-- Add temptation -->
<!-- Add item -->
<form class="panel panel-acid space-y-4 p-6" onsubmit={addItem}>
<p class="label">add a temptation</p>
<input class="field" bind:value={title} maxlength="200" placeholder="what you covet" />
<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 URL (we'll keep vigil on the price)" />
<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)" />
{#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 ? 'coveting…' : 'covet it +'}</button>
<button class="btn btn-acid" disabled={busy}>{busy ? 'adding…' : 'add item +'}</button>
</form>
{#if !loaded}
<p class="text-center text-mute flicker">unveiling temptations…</p>
<p class="text-center text-mute flicker">loading items…</p>
{:else if loadError}
<p class="border-2 border-rose bg-rose/10 px-3 py-2 text-sm text-rose">{loadError}</p>
{:else if items.length === 0}
<div class="panel p-10 text-center">
<p class="gospel text-2xl">this list is bare</p>
<p class="mt-2 text-mute">paste a craving above to begin.</p>
<p class="gospel text-2xl">this list is empty</p>
<p class="mt-2 text-mute">add something you want above to begin.</p>
</div>
{:else}
<ul class="space-y-3">
@@ -265,7 +320,7 @@
<!-- Status is the primary control: a real, cyclable badge. -->
<button
class="tag shrink-0 cursor-pointer transition hover:brightness-125 {STATUS_STYLE[item.status]}"
title="click to cycle: coveted → acquired → renounced"
title="click to cycle: want → bought → skip"
onclick={() => cycleStatus(item)}
>
{STATUS_LABEL[item.status]}
@@ -277,7 +332,7 @@
{#if item.url}
<button
class="rounded border border-smoke px-2 py-1 text-mute transition hover:border-iris hover:text-iris disabled:opacity-40"
title="refetch price now (keep vigil)"
title="check the price now"
disabled={refetchingId === item.id}
onclick={() => refetchItem(item)}
>
@@ -285,12 +340,19 @@
</button>
<button
class="rounded border border-smoke px-2 py-1 text-mute transition hover:border-iris hover:text-iris"
title="price history (the chronicle)"
title="price history"
onclick={() => toggleHistory(item)}
>
{historyFor === item.id ? 'hide history' : 'history'}
</button>
{/if}
<button
class="rounded border border-smoke px-2 py-1 text-mute transition hover:border-iris hover:text-iris"
title="edit this item"
onclick={() => (editingId === item.id ? cancelEdit() : startEdit(item))}
>
{editingId === item.id ? 'close' : '✎ edit'}
</button>
<button
class="rounded border border-smoke px-2 py-1 text-mute transition hover:border-rose hover:text-rose"
title="remove from this list"
@@ -300,19 +362,49 @@
</button>
</div>
{#if editingId === item.id}
<div class="space-y-3 border-t border-smoke pt-3">
<input class="field" bind:value={edit.title} maxlength="200" placeholder="title" />
<input class="field" bind:value={edit.url} placeholder="product URL" />
<div class="grid gap-3 sm:grid-cols-[1fr_8rem]">
<input
class="field"
bind:value={edit.target}
inputmode="decimal"
placeholder="target price (blank = notify on any drop)"
/>
<input
class="field"
bind:value={edit.currency}
maxlength="3"
placeholder="currency"
title="3-letter code, e.g. EUR — overrides the detected currency"
/>
</div>
<input class="field" bind:value={edit.note} maxlength="1000" placeholder="note" />
{#if editError}<p class="text-xs text-rose">{editError}</p>{/if}
<div class="flex justify-end gap-2 text-xs">
<button class="rounded border border-smoke px-3 py-1 text-mute hover:text-ink" onclick={cancelEdit}>cancel</button>
<button class="btn btn-acid px-3 py-1" disabled={editBusy} onclick={() => saveEdit(item)}>
{editBusy ? 'saving…' : 'save'}
</button>
</div>
</div>
{/if}
{#if onSale(item)}
<p class="gospel text-sm text-mint"> the price has fallen — your moment is upon you</p>
<p class="gospel text-sm text-mint">✦ price dropped — grab it now</p>
{/if}
{#if item.last_error}
<p class="text-xs text-rose">vigil faltered: {item.last_error}</p>
<p class="text-xs text-rose">price check failed: {item.last_error}</p>
{/if}
{#if historyFor === item.id}
<div class="border-t border-smoke pt-3">
{#if historyLoading}
<p class="text-xs text-mute flicker">unrolling the chronicle</p>
<p class="text-xs text-mute flicker">loading history</p>
{:else if history.length === 0}
<p class="text-xs text-mute">no observations yet — keep vigil to begin the record.</p>
<p class="text-xs text-mute">no price checks yet — hit refresh to start tracking.</p>
{:else}
<ul class="space-y-1 text-xs">
{#each history as h}