From b1c62e65a466056653e00ca3d3ecc7e0cec0fe0d Mon Sep 17 00:00:00 2001 From: mikewheeleer Date: Thu, 30 Jul 2026 12:19:41 +0530 Subject: [PATCH] feat: persist marketplace watchlist --- .../marketplace/SavedListingsRail.tsx | 50 +++++++++++++++++++ src/hooks/useMarketplaceWatchlist.test.ts | 22 ++++++++ src/hooks/useMarketplaceWatchlist.ts | 37 ++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/components/marketplace/SavedListingsRail.tsx create mode 100644 src/hooks/useMarketplaceWatchlist.test.ts create mode 100644 src/hooks/useMarketplaceWatchlist.ts diff --git a/src/components/marketplace/SavedListingsRail.tsx b/src/components/marketplace/SavedListingsRail.tsx new file mode 100644 index 00000000..7a794dd6 --- /dev/null +++ b/src/components/marketplace/SavedListingsRail.tsx @@ -0,0 +1,50 @@ +'use client'; + +export interface SavedListing { + id: string; + title: string; + price: string; + status?: 'Active' | 'Sold' | 'Cancelled'; +} + +export function SavedListingsRail({ + listings, + savedIds, + onRemove, + onSelect, +}: { + listings: SavedListing[]; + savedIds: ReadonlySet; + onRemove: (id: string) => void; + onSelect?: (id: string) => void; +}) { + const saved = listings.filter((listing) => savedIds.has(listing.id)); + if (saved.length === 0) return null; + + return ( +
+

Saved

+
    + {saved.map((listing) => ( +
  • + + +
  • + ))} +
+
+ ); +} diff --git a/src/hooks/useMarketplaceWatchlist.test.ts b/src/hooks/useMarketplaceWatchlist.test.ts new file mode 100644 index 00000000..88c09368 --- /dev/null +++ b/src/hooks/useMarketplaceWatchlist.test.ts @@ -0,0 +1,22 @@ +/** @vitest-environment happy-dom */ + +import { describe, expect, it } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { + MARKETPLACE_WATCHLIST_KEY, + useMarketplaceWatchlist, +} from './useMarketplaceWatchlist'; + +describe('useMarketplaceWatchlist', () => { + it('restores and persists saved listing ids', () => { + localStorage.setItem(MARKETPLACE_WATCHLIST_KEY, JSON.stringify(['listing-1'])); + const { result } = renderHook(() => useMarketplaceWatchlist()); + + expect(result.current.isSaved('listing-1')).toBe(true); + act(() => result.current.toggle('listing-2')); + expect(JSON.parse(localStorage.getItem(MARKETPLACE_WATCHLIST_KEY) ?? '[]')).toEqual([ + 'listing-1', + 'listing-2', + ]); + }); +}); diff --git a/src/hooks/useMarketplaceWatchlist.ts b/src/hooks/useMarketplaceWatchlist.ts new file mode 100644 index 00000000..13939e7c --- /dev/null +++ b/src/hooks/useMarketplaceWatchlist.ts @@ -0,0 +1,37 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; + +export const MARKETPLACE_WATCHLIST_KEY = 'commitlabs:marketplace-watchlist'; + +function readWatchlist(): string[] { + try { + const value: unknown = JSON.parse(window.localStorage.getItem(MARKETPLACE_WATCHLIST_KEY) ?? '[]'); + return Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string') : []; + } catch { + return []; + } +} + +export function useMarketplaceWatchlist() { + const [savedIds, setSavedIds] = useState([]); + + useEffect(() => setSavedIds(readWatchlist()), []); + + useEffect(() => { + window.localStorage.setItem(MARKETPLACE_WATCHLIST_KEY, JSON.stringify(savedIds)); + }, [savedIds]); + + const toggle = useCallback((listingId: string) => { + setSavedIds((current) => + current.includes(listingId) + ? current.filter((id) => id !== listingId) + : [...current, listingId], + ); + }, []); + + const isSaved = useCallback((listingId: string) => savedIds.includes(listingId), [savedIds]); + const savedSet = useMemo(() => new Set(savedIds), [savedIds]); + + return { savedIds, savedSet, isSaved, toggle }; +}