updated wording
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user