From b0360927753ff986f0e9d151c8d631465a380e75 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Thu, 10 Sep 2026 02:39:42 -0400 Subject: [PATCH] feat: expand catalog sorting --- .../repositories/listing-repository.js | 8 +++++- .../src/modules/search/domain/search-query.js | 4 +-- apps/api/tests/domain/search-query.test.js | 3 ++ .../components/atoms/CatalogSortSelect.scss | 18 ++++++++++++ .../atoms/CatalogSortSelect.stories.js | 6 ++++ .../components/atoms/CatalogSortSelect.vue | 28 +++++++++++++++++++ apps/web/src/pages/CatalogPage.vue | 10 ++----- docs/IMPLEMENTATION-STATUS.md | 1 + 8 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/components/atoms/CatalogSortSelect.scss create mode 100644 apps/web/src/components/atoms/CatalogSortSelect.stories.js create mode 100644 apps/web/src/components/atoms/CatalogSortSelect.vue diff --git a/apps/api/src/modules/listings/repositories/listing-repository.js b/apps/api/src/modules/listings/repositories/listing-repository.js index a10fa22..c0c3364 100644 --- a/apps/api/src/modules/listings/repositories/listing-repository.js +++ b/apps/api/src/modules/listings/repositories/listing-repository.js @@ -135,7 +135,13 @@ export function createListingRepository(db) { const offset = (page - 1) * limit; const sortOrder = - sort === "price_low" + sort === "updated" + ? desc(listings.updatedAt) + : sort === "name_asc" + ? products.name + : sort === "name_desc" + ? desc(products.name) + : sort === "price_low" ? listings.askingPriceMinor : sort === "price_high" ? desc(listings.askingPriceMinor) diff --git a/apps/api/src/modules/search/domain/search-query.js b/apps/api/src/modules/search/domain/search-query.js index b224776..b37e7e5 100644 --- a/apps/api/src/modules/search/domain/search-query.js +++ b/apps/api/src/modules/search/domain/search-query.js @@ -58,8 +58,8 @@ export function createSearchQuery({ throw new Error("minPriceMinor must be <= maxPriceMinor"); } - if (!["recent", "price_low", "price_high"].includes(sort)) { - throw new Error("sort must be recent, price_low, or price_high"); + if (!["recent", "updated", "name_asc", "name_desc", "price_low", "price_high"].includes(sort)) { + throw new Error("sort must be recent, updated, name_asc, name_desc, price_low, or price_high"); } return { diff --git a/apps/api/tests/domain/search-query.test.js b/apps/api/tests/domain/search-query.test.js index 54cef33..6c57159 100644 --- a/apps/api/tests/domain/search-query.test.js +++ b/apps/api/tests/domain/search-query.test.js @@ -57,6 +57,9 @@ test("builds a public query filter set that hides sold items by default", () => test("accepts only the supported public search sort values", () => { assert.equal(createSearchQuery({ sort: "price_low" }).sort, "price_low"); + assert.equal(createSearchQuery({ sort: "updated" }).sort, "updated"); + assert.equal(createSearchQuery({ sort: "name_asc" }).sort, "name_asc"); + assert.equal(createSearchQuery({ sort: "name_desc" }).sort, "name_desc"); assert.throws( () => createSearchQuery({ sort: "random" }), /sort must be recent/, diff --git a/apps/web/src/components/atoms/CatalogSortSelect.scss b/apps/web/src/components/atoms/CatalogSortSelect.scss new file mode 100644 index 0000000..699242b --- /dev/null +++ b/apps/web/src/components/atoms/CatalogSortSelect.scss @@ -0,0 +1,18 @@ +.catalog-sort-select { + display: grid; + gap: var(--space-1); +} + +.catalog-sort-select span { + color: var(--color-text-muted); + font-size: var(--text-sm); +} + +.catalog-sort-select select { + min-height: 44px; + border: var(--border-thin) solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-3); + background: var(--color-surface); + color: var(--color-text); +} diff --git a/apps/web/src/components/atoms/CatalogSortSelect.stories.js b/apps/web/src/components/atoms/CatalogSortSelect.stories.js new file mode 100644 index 0000000..3569430 --- /dev/null +++ b/apps/web/src/components/atoms/CatalogSortSelect.stories.js @@ -0,0 +1,6 @@ +import CatalogSortSelect from './CatalogSortSelect.vue'; + +export default { title: 'Atoms/CatalogSortSelect', component: CatalogSortSelect }; + +export const RecentlyAdded = { args: { modelValue: 'recent' } }; +export const NameAscending = { args: { modelValue: 'name_asc' } }; diff --git a/apps/web/src/components/atoms/CatalogSortSelect.vue b/apps/web/src/components/atoms/CatalogSortSelect.vue new file mode 100644 index 0000000..abe5fb6 --- /dev/null +++ b/apps/web/src/components/atoms/CatalogSortSelect.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/web/src/pages/CatalogPage.vue b/apps/web/src/pages/CatalogPage.vue index 69688db..dc309e4 100644 --- a/apps/web/src/pages/CatalogPage.vue +++ b/apps/web/src/pages/CatalogPage.vue @@ -8,6 +8,7 @@ import { listPublicConditions, } from "@/services/catalog-service.js"; import ListingCard from "@/components/molecules/ListingCard.vue"; +import CatalogSortSelect from "@/components/atoms/CatalogSortSelect.vue"; const route = useRoute(); const router = useRouter(); @@ -124,14 +125,7 @@ watch( >Max price (cents) - + diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index f5140da..e7c3939 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -163,6 +163,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro - `npm audit --omit=dev` currently reports zero production vulnerabilities. Re-run the audit after dependency changes and review development-dependency findings separately before production releases. - Authenticated E2E coverage now verifies both the core catalog workflow and the Settings, Saved Views, and export-controls workflow. - Production dependency audit currently reports zero vulnerabilities with `npm audit --omit=dev`. +- Public catalog sorting now supports recently added, recently updated, name A-Z, name Z-A, price low-to-high, and price high-to-low through a URL-synchronized atomic sort control. Broader inventory filter parity and market-value sorting remain. ## Local Commands