177 lines
4.7 KiB
TypeScript
177 lines
4.7 KiB
TypeScript
import { api } from "./api";
|
|
|
|
export type ItemStatus = "coveted" | "acquired" | "renounced";
|
|
|
|
export type List = {
|
|
id: string;
|
|
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;
|
|
title: string;
|
|
url: string | null;
|
|
note: string | null;
|
|
status: ItemStatus;
|
|
target_price: number | null;
|
|
position: number;
|
|
// Filled by the Phase 3 fetcher; null until then.
|
|
title_fetched: string | null;
|
|
current_price: number | null;
|
|
currency: string | null;
|
|
image_url: string | null;
|
|
in_stock: boolean | null;
|
|
source: string | null;
|
|
fetched_at: string | null;
|
|
track_enabled: boolean;
|
|
last_error: string | null;
|
|
checked_at: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type PricePoint = {
|
|
price: number;
|
|
currency: string;
|
|
in_stock: boolean | null;
|
|
fetched_at: string;
|
|
};
|
|
|
|
export type NewList = {
|
|
name: string;
|
|
emoji?: string | null;
|
|
description?: string | null;
|
|
};
|
|
export type NewItem = {
|
|
title: string;
|
|
url?: string | null;
|
|
note?: string | null;
|
|
target_price?: number | null;
|
|
currency?: string | null;
|
|
};
|
|
|
|
// ---- Lists ----------------------------------------------------------------
|
|
|
|
export const listsApi = {
|
|
all: () => api.get<List[]>("/lists"),
|
|
create: (b: NewList) => api.post<List>("/lists", b),
|
|
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) =>
|
|
api.post<Item>(`/lists/${listId}/items`, b),
|
|
updateItem: (
|
|
id: string,
|
|
b: Partial<NewItem> & { status?: ItemStatus; position?: number },
|
|
) => api.patch<Item>(`/items/${id}`, b),
|
|
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. */
|
|
class ListsStore {
|
|
items = $state<List[]>([]);
|
|
loaded = $state(false);
|
|
|
|
async load() {
|
|
this.items = await listsApi.all();
|
|
this.loaded = true;
|
|
}
|
|
|
|
async create(b: NewList): Promise<List> {
|
|
const created = await listsApi.create(b);
|
|
this.items.push(created);
|
|
return created;
|
|
}
|
|
|
|
async remove(id: string) {
|
|
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();
|