From 9bdd5c491080ae6b16aeefb76c0c35908ae30dd8 Mon Sep 17 00:00:00 2001 From: mariosemes Date: Thu, 26 Mar 2026 21:43:32 +0100 Subject: [PATCH] Replace results cards with sortable, filterable table - Compact table view with thumbnail, product name, price, store, link - Click column headers to sort (toggles asc/desc) - Filter bar to search within results by product name - Store dropdown filter when multiple stores present - Sticky header, hover-reveal "Open" link, sort direction arrows - Shows "X of Y shown" count when filtering Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/src/routes/results/+page.svelte | 226 +++++++++++++++------ 1 file changed, 169 insertions(+), 57 deletions(-) diff --git a/src/client/src/routes/results/+page.svelte b/src/client/src/routes/results/+page.svelte index 2c7f7c3..b281081 100644 --- a/src/client/src/routes/results/+page.svelte +++ b/src/client/src/routes/results/+page.svelte @@ -9,6 +9,9 @@ let meta = $state(null); let loading = $state(true); let sortBy = $state('price'); + let sortAsc = $state(true); + let filterText = $state(''); + let filterStore = $state(''); onMount(async () => { const params = $page.url.searchParams; @@ -44,15 +47,53 @@ doSearch(); } - let sortedResults = $derived(() => { - const sorted = [...results]; - switch (sortBy) { - case 'price': sorted.sort((a, b) => (a.price ?? Infinity) - (b.price ?? Infinity)); break; - case 'price-desc': sorted.sort((a, b) => (b.price ?? -Infinity) - (a.price ?? -Infinity)); break; - case 'name': sorted.sort((a, b) => a.name.localeCompare(b.name)); break; - case 'store': sorted.sort((a, b) => a.storeName.localeCompare(b.storeName)); break; + function handleSort(column) { + if (sortBy === column) { + sortAsc = !sortAsc; + } else { + sortBy = column; + sortAsc = column === 'name' || column === 'store'; } - return sorted; + } + + // Unique store names for the filter dropdown + let storeNames = $derived(() => { + const names = [...new Set(results.map(r => r.storeName))]; + return names.sort(); + }); + + let filteredAndSorted = $derived(() => { + let items = [...results]; + + // Text filter + if (filterText) { + const lower = filterText.toLowerCase(); + items = items.filter(r => r.name.toLowerCase().includes(lower)); + } + + // Store filter + if (filterStore) { + items = items.filter(r => r.storeName === filterStore); + } + + // Sort + items.sort((a, b) => { + let cmp = 0; + switch (sortBy) { + case 'price': + cmp = (a.price ?? Infinity) - (b.price ?? Infinity); + break; + case 'name': + cmp = a.name.localeCompare(b.name); + break; + case 'store': + cmp = a.storeName.localeCompare(b.storeName); + break; + } + return sortAsc ? cmp : -cmp; + }); + + return items; }); function formatPrice(price, currency) { @@ -60,6 +101,11 @@ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price); } catch { return `${currency} ${price.toFixed(2)}`; } } + + function sortIcon(column) { + if (sortBy !== column) return ''; + return sortAsc ? '\u2191' : '\u2193'; + }
@@ -71,25 +117,17 @@ fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> -
-
- {#if meta} - - {meta.totalResults} results from {meta.storeCount} stores in {meta.duration}ms - - {/if} - -
+ {#if meta} + + {meta.totalResults} results from {meta.storeCount} stores in {meta.duration}ms + + {/if} @@ -103,19 +141,43 @@ {/if} + + {#if !loading && results.length > 0} +
+
+ + + + +
+ + {#if storeNames().length > 1} + + {/if} + + + {filteredAndSorted().length} of {results.length} shown + +
+ {/if} + -