From 299ad7a1a6ddde05587b72400ce585200988ea81 Mon Sep 17 00:00:00 2001 From: Victor Noguera Date: Mon, 13 Apr 2026 03:04:28 -0400 Subject: [PATCH] feat: enhance product listing with additional metrics and sorting options --- category-blacklist.csv | 5 ++++- src/server.ts | 29 ++++++++++++++++++++++++++++- src/web/frontend.tsx | 40 +++++++++++++++++++++------------------- src/web/styles.css | 7 +++++++ 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/category-blacklist.csv b/category-blacklist.csv index c4c2ef8..986ff66 100644 --- a/category-blacklist.csv +++ b/category-blacklist.csv @@ -1,4 +1,7 @@ id,name +19419898011,Amazon Explore +14297978011,Online Learning 229534,Software 283155,Books -16310101,Grocery Gourmet Food \ No newline at end of file +16310101,Grocery Gourmet Food +599858,Magazine Subscriptions \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 8acfe6c..314676a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -27,6 +27,10 @@ type ProductListRecord = { verdict: "FBA" | "FBM" | "SKIP"; confidence: number | null; sellability_status: string | null; + monthly_sold: number | null; + seller_count: number | null; + sales_rank: number | null; + current_price: number | null; fetched_at: string; }; @@ -274,6 +278,21 @@ function getProductList(filters: URLSearchParams) { } const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const allowedSort = new Set([ + "asin", + "verdict", + "monthly_sold", + "seller_count", + "sales_rank", + "current_price", + "product_name", + "fetched_at", + ]); + const orderBy = parseResultSort( + filters.get("sort"), + allowedSort, + "CAST(COALESCE(monthly_sold, 0) AS INTEGER) DESC, fetched_at DESC", + ); const baseUnion = ` SELECT @@ -286,6 +305,10 @@ function getProductList(filters: URLSearchParams) { verdict, confidence, sellability_status, + monthly_sold, + sellers AS seller_count, + sales_rank, + current_price, fetched_at FROM results UNION ALL @@ -299,6 +322,10 @@ function getProductList(filters: URLSearchParams) { verdict, confidence, sellability_status, + monthly_sold, + seller_count, + sales_rank, + current_price, fetched_at FROM product_analysis_results `; @@ -308,7 +335,7 @@ function getProductList(filters: URLSearchParams) { .get(...params) as { total: number }; const items = db - .query(`SELECT * FROM (${baseUnion}) all_products ${where} ORDER BY fetched_at DESC LIMIT ? OFFSET ?`) + .query(`SELECT * FROM (${baseUnion}) all_products ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`) .all(...params, pageSize, offset) as ProductListRecord[]; return { diff --git a/src/web/frontend.tsx b/src/web/frontend.tsx index f326fbd..454cba5 100644 --- a/src/web/frontend.tsx +++ b/src/web/frontend.tsx @@ -87,6 +87,10 @@ type ProductListItem = { verdict: "FBA" | "FBM" | "SKIP"; confidence: number | null; sellability_status: string | null; + monthly_sold: number | null; + seller_count: number | null; + sales_rank: number | null; + current_price: number | null; fetched_at: string; }; @@ -615,6 +619,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () = const [activeVerdict, setActiveVerdict] = useState(verdict); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); + const [sort, setSort] = useState({ field: "monthly_sold", direction: "DESC" }); useEffect(() => { setActiveVerdict(verdict); @@ -626,6 +631,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () = async function load() { setLoading(true); const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); + params.set("sort", buildSortValue(sort)); if (search) params.set("q", search); if (activeVerdict) params.set("verdict", activeVerdict); const res = await fetch(`/api/products?${params.toString()}`); @@ -640,7 +646,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () = return () => { cancelled = true; }; - }, [search, activeVerdict, page, pageSize]); + }, [search, activeVerdict, page, pageSize, sort]); return (
@@ -667,36 +673,32 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () = - - - - - - - - - + + + + + + + {loading ? ( - + ) : items?.items.length ? ( items.items.map((item) => ( - - - - - - - + + + + + )) ) : ( - + )}
ASINVerdictProductBrandCategoryConfidenceProcessRun IDFetched
Loading...
Loading...
{item.asin} {item.verdict}{item.product_name || "-"}{item.brand || "-"}{item.category || "-"}{formatNumber(item.confidence)}{item.processType}{item.runId}{formatDate(item.fetched_at)}{formatNumber(item.monthly_sold)}{formatNumber(item.seller_count)}{formatNumber(item.sales_rank)}{formatCurrency(item.current_price)}{item.product_name || "-"}
No products found
No products found
diff --git a/src/web/styles.css b/src/web/styles.css index febf273..b1893a9 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -84,6 +84,13 @@ td { overflow-wrap: anywhere; } +.product-col { + min-width: 300px; + max-width: 440px; + white-space: normal; + overflow-wrap: anywhere; +} + th { background: #fafafb; font-weight: 600;