added sharing and subscriptions
This commit is contained in:
@@ -80,6 +80,29 @@
|
||||
coords.length ? coords.reduce((hi, c, i) => (c.price > coords[hi].price ? i : hi), 0) : -1
|
||||
);
|
||||
|
||||
// Out-of-stock spans — shade the time the item couldn't be bought.
|
||||
// Each maximal run of in_stock===false becomes a band, extended halfway
|
||||
// to its in-stock neighbours so single checks still read as a region.
|
||||
const oosBands = $derived.by(() => {
|
||||
if (!coords.length) return [];
|
||||
const bands: { x: number; w: number }[] = [];
|
||||
let start: number | null = null;
|
||||
for (let i = 0; i <= coords.length; i++) {
|
||||
const oos = i < coords.length && coords[i].in_stock === false;
|
||||
if (oos && start === null) start = i;
|
||||
if (!oos && start !== null) {
|
||||
const end = i - 1;
|
||||
const left = start > 0 ? (coords[start - 1].cx + coords[start].cx) / 2 : coords[start].cx;
|
||||
const right =
|
||||
end < coords.length - 1 ? (coords[end].cx + coords[end + 1].cx) / 2 : coords[end].cx;
|
||||
bands.push({ x: left, w: Math.max(right - left, 3) });
|
||||
start = null;
|
||||
}
|
||||
}
|
||||
return bands;
|
||||
});
|
||||
const anyOos = $derived(coords.some((c) => c.in_stock === false));
|
||||
|
||||
const targetY = $derived(target != null && stats ? y(target) : null);
|
||||
const latest = $derived(coords.length ? coords[coords.length - 1] : null);
|
||||
const onSale = $derived(latest != null && target != null && latest.price <= target);
|
||||
@@ -168,6 +191,18 @@
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Out-of-stock spans — rose shade behind everything -->
|
||||
{#each oosBands as b}
|
||||
<rect
|
||||
x={b.x}
|
||||
y={PAD.t}
|
||||
width={b.w}
|
||||
height={innerH}
|
||||
fill="var(--color-rose)"
|
||||
opacity="0.1"
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<!-- Gridlines + price ticks -->
|
||||
{#each gridLines as g}
|
||||
<line
|
||||
@@ -270,6 +305,9 @@
|
||||
<span class="flex items-center gap-3 text-mute">
|
||||
<span class="flex items-center gap-1"><span class="inline-block size-2 rounded-full" style="background:var(--color-gold)"></span>low {fmtMoney(coords[lowIdx].price)}</span>
|
||||
<span class="flex items-center gap-1"><span class="inline-block size-2 rounded-full" style="background:var(--color-rose)"></span>high {fmtMoney(coords[highIdx].price)}</span>
|
||||
{#if anyOos}
|
||||
<span class="flex items-center gap-1"><span class="inline-block h-2 w-3 rounded-sm" style="background:var(--color-rose);opacity:0.35"></span>out of stock</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if latest}
|
||||
<span class:text-mint={onSale} class:text-ink={!onSale}>
|
||||
|
||||
@@ -7,11 +7,31 @@ export type List = {
|
||||
name: string;
|
||||
emoji: string | null;
|
||||
description: string | null;
|
||||
share_token: string | null;
|
||||
position: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type SharedView = { list: List; items: Item[] };
|
||||
|
||||
export type Subscription = {
|
||||
id: string;
|
||||
kind: "list" | "item";
|
||||
created_at: string;
|
||||
list_id: string | null;
|
||||
item_id: string | null;
|
||||
title: string;
|
||||
emoji: string | null;
|
||||
share_token: string | null;
|
||||
url: string | null;
|
||||
image_url: string | null;
|
||||
current_price: number | null;
|
||||
currency: string | null;
|
||||
in_stock: boolean | null;
|
||||
target_price: number | null;
|
||||
};
|
||||
|
||||
export type Item = {
|
||||
id: string;
|
||||
list_id: string;
|
||||
@@ -64,6 +84,9 @@ export const listsApi = {
|
||||
update: (id: string, b: Partial<NewList> & { position?: number }) =>
|
||||
api.patch<List>(`/lists/${id}`, b),
|
||||
remove: (id: string) => api.del<{ deleted: string }>(`/lists/${id}`),
|
||||
share: (id: string) => api.post<List>(`/lists/${id}/share`, {}),
|
||||
unshare: (id: string) => api.del<List>(`/lists/${id}/share`),
|
||||
shared: (token: string) => api.get<SharedView>(`/shared/${token}`),
|
||||
|
||||
items: (listId: string) => api.get<Item[]>(`/lists/${listId}/items`),
|
||||
addItem: (listId: string, b: NewItem) =>
|
||||
@@ -75,6 +98,11 @@ export const listsApi = {
|
||||
removeItem: (id: string) => api.del<{ deleted: string }>(`/items/${id}`),
|
||||
refetch: (id: string) => api.post<Item>(`/items/${id}/refetch`, {}),
|
||||
history: (id: string) => api.get<PricePoint[]>(`/items/${id}/history`),
|
||||
|
||||
subscriptions: () => api.get<Subscription[]>("/subscriptions"),
|
||||
subscribe: (b: { list_id?: string; item_id?: string }) =>
|
||||
api.post<{ id: string }>("/subscriptions", b),
|
||||
unsubscribe: (id: string) => api.del<{ deleted: string }>(`/subscriptions/${id}`),
|
||||
};
|
||||
|
||||
/** Reactive store for the user's lists. */
|
||||
@@ -97,6 +125,52 @@ class ListsStore {
|
||||
await listsApi.remove(id);
|
||||
this.items = this.items.filter((l) => l.id !== id);
|
||||
}
|
||||
|
||||
/** Swap in an updated list (e.g. after share/unshare). */
|
||||
replace(list: List) {
|
||||
const i = this.items.findIndex((l) => l.id === list.id);
|
||||
if (i >= 0) this.items[i] = list;
|
||||
}
|
||||
}
|
||||
|
||||
export const lists = new ListsStore();
|
||||
|
||||
/** The current user's subscriptions, with helpers to toggle by list/item. */
|
||||
class SubsStore {
|
||||
items = $state<Subscription[]>([]);
|
||||
loaded = $state(false);
|
||||
|
||||
async load() {
|
||||
this.items = await listsApi.subscriptions();
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/** Existing subscription id for a list, or null. */
|
||||
forList(listId: string): string | null {
|
||||
return this.items.find((s) => s.list_id === listId)?.id ?? null;
|
||||
}
|
||||
|
||||
/** Existing subscription id for an item, or null. */
|
||||
forItem(itemId: string): string | null {
|
||||
return this.items.find((s) => s.item_id === itemId)?.id ?? null;
|
||||
}
|
||||
|
||||
async subscribeList(listId: string) {
|
||||
if (this.forList(listId)) return;
|
||||
await listsApi.subscribe({ list_id: listId });
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async subscribeItem(itemId: string) {
|
||||
if (this.forItem(itemId)) return;
|
||||
await listsApi.subscribe({ item_id: itemId });
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async unsubscribe(id: string) {
|
||||
await listsApi.unsubscribe(id);
|
||||
this.items = this.items.filter((s) => s.id !== id);
|
||||
}
|
||||
}
|
||||
|
||||
export const subs = new SubsStore();
|
||||
|
||||
Reference in New Issue
Block a user