Wishlists & Collections โ Architecture Guide
The Wishlists & Collections system enables Visitors and Buyers to save Products, Services, and Malets into named, user-defined lists with per-item quantities and notes. It uses a dual-persistence architecture: local-first (localStorage + IndexedDB) for instant UI, with background GraphQL sync to the nodes subgraph when authenticated.
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UI Layer โ
โ โ
โ CardQuickMenu.svelte โโ /wishlist (two-panel page) โ
โ (Save to List picker) SideNav โโ UserMenu โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ wishlistStore.svelte.ts โ
โ (Svelte 5 runes โ $state) โ
โ - items: ListItem[] โ
โ - lists: CustomList[] โ
โ - loaded / syncing flags โ
โ - idMap: localโbackend ID resolution โ
โโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโผโโโโโโโ โโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ localStorage โ โ GraphQL (nodes) โ
โ + IndexedDB โ โ collections / items โ
โ (instant) โ โ (background sync) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
Persistence Strategy
The store uses a three-tier persistence model:
| Tier | Storage | Speed | Purpose |
|---|---|---|---|
| 1. localStorage | Sync | Instant | Page reload hydration โ read synchronously before first render |
| 2. IndexedDB | Async | Fast | Large data capacity, survives browser restarts |
| 3. GraphQL Backend | Network | Variable | Cross-device sync, authenticated users only |
Write Path
Every mutation (add/remove/update) follows:
- Optimistic update โ
$statearrays mutated immediately - localStorage sync โ
JSON.stringifywritten synchronously - IndexedDB write โ
idbSet(fire-and-forget async) - Backend sync โ GraphQL mutation (fire-and-forget, errors logged to console)
Read Path (on load)
- localStorage read (synchronous โ instant hydration)
- IndexedDB read (async โ may override localStorage with fresher data)
- Backend fetch (async โ merges server collections into local state, if authenticated)
Components
`CardQuickMenu.svelte`
The primary entry point for saving items. Lives on product cards, service cards, and lobby tiles.
Key derived values:
let effectiveId = $derived(itemId || product?.id || '');
let effectiveName = $derived(itemName || product?.name || '');
let effectiveHandle = $derived(maletHandle || product?.maletHandle || '');
let effectiveSlug = $derived(product?.slug || '');
These derived values ensure the malet handle and slug are always resolved, even when individual props aren't passed โ falling back to the product (ComparisonProduct) object.
`/wishlist` Route
A two-panel management page. SSR is disabled (ssr = false) because the page reads from IndexedDB/localStorage.
Left sidebar: List names with item counts, create/rename/delete actions. Main area: Item grid/list with quantity steppers, inline notes, price totals, CSV export.
Item links resolve to /{maletHandle}/item/{slug || id}. If maletHandle is empty, falls back to /search?q={name}.
Navigation Entry Points
- SideNav (
primaryRoutesarray) โ "My Lists" withactions/hearticon - UserMenu (avatar dropdown) โ "My Lists" button after "My Murchases"
Store: `wishlistStore.svelte.ts`
Types
interface ListItem {
id: string;
type: 'product' | 'service' | 'malet';
name: string;
handle?: string;
slug?: string;
imageUrl?: string;
price?: number;
currency?: string;
maletHandle: string;
maletName: string;
addedAt: number;
listId: string;
quantity: number;
note?: string;
}
interface CustomList {
id: string;
name: string;
emoji: string;
createdAt: number;
description?: string;
visibility: 'private' | 'unlisted' | 'public';
}
Actions
| Action | Signature | Backend Sync |
|---|---|---|
addToList |
(item, listId?) โ void |
addCollectionItem |
removeFromList |
(itemId, listId?) โ void |
removeCollectionItem |
toggleItem |
(item, listId?) โ boolean |
Toggle add/remove |
moveToList |
(itemId, from, to) โ void |
โ |
updateItemQuantity |
(itemId, qty, listId?) โ void |
updateCollectionItem |
updateItemNote |
(itemId, note, listId?) โ void |
updateCollectionItem |
createList |
(name, emoji?) โ CustomList |
createCollection |
renameList |
(id, name) โ void |
updateCollection |
deleteList |
(id) โ void |
deleteCollection |
exportListCSV |
(listId) โ void |
โ (client-only) |
ID Resolution
The store maintains an idMap: Map<string, string> that maps local IDs to backend IDs:
- List IDs:
col_<timestamp>โ backend collection ID (mapped aftercreateCollectionresponse) - Item IDs:
<entityId>:<listId>โ backend CollectionItem ID (mapped afteraddCollectionItemresponse)
GraphQL API (`$lib/queries/collections.ts`)
Queries
| Query | Variables | Description |
|---|---|---|
collections |
first: Int |
Fetch authenticated user's collections |
collectionItems |
collectionId: String!, first: Int |
Fetch items in a collection |
Mutations
| Mutation | Input | Description |
|---|---|---|
createCollection |
CreateCollectionInput |
Create a new named collection |
updateCollection |
UpdateCollectionInput |
Rename or change visibility |
deleteCollection |
id: String! |
Delete + cascade item cleanup |
addCollectionItem |
AddCollectionItemInput |
Add an entity to a collection |
removeCollectionItem |
RemoveCollectionItemInput |
Remove an item by backend ID |
updateCollectionItem |
UpdateCollectionItemInput |
Update quantity or note |
All mutations are guarded by GqlAuthGuard and ownership checks in the backend resolver.
Migration
The store includes a migrateItems() function that handles legacy data:
collectionIdโlistId(field rename)- Missing
quantityโ defaults to1 - Missing
maletHandleโ falls back tohandleor'unknown'
Integration Guide
To add the "Save to List" action to a new surface:
- Import
CardQuickMenufrom$lib/components/catalog/CardQuickMenu.svelte - Pass product metadata props:
itemId,itemName,imageUrl,price,currency,maletHandle,maletName - Optionally pass a full
product(ComparisonProduct) object for slug and handle resolution - The component handles all state management internally
<CardQuickMenu
product={comparisonProduct}
type="product"
itemId={product.id}
itemName={product.name}
imageUrl={coverImage}
price={product.basePrice}
currency="USD"
maletHandle={maletHandle}
maletName={maletName}
/>
Related
- Social Graph โ Follow system and user connections
- Privacy & Security APIs โ Content visibility and data privacy controls
- Community Features โ Social graph and inter-Visitor interactions