diff --git a/.local/project-ports.json b/.local/project-ports.json index 6f06375..cd79ac8 100644 --- a/.local/project-ports.json +++ b/.local/project-ports.json @@ -2,8 +2,8 @@ "version": 1, "projectName": "pokemon-inventory", "projectPath": "/Users/fragginwagon/Developer/Source Code/pokemon/pokemon-inventory", - "updatedAt": "2026-09-07T15:05:39.264Z", - "lastSeenAt": "2026-09-07T15:05:39.264Z", + "updatedAt": "2026-09-08T23:54:46.326Z", + "lastSeenAt": "2026-09-08T23:54:46.326Z", "ports": [ { "service": "api", @@ -11,6 +11,12 @@ "port": 3000, "notes": "" }, + { + "service": "web", + "protocol": "tcp", + "port": 5173, + "notes": "" + }, { "service": "postgres", "protocol": "tcp", diff --git a/apps/web/src/assets/styles/global.scss b/apps/web/src/assets/styles/global.scss index 88c32fe..86ef744 100644 --- a/apps/web/src/assets/styles/global.scss +++ b/apps/web/src/assets/styles/global.scss @@ -35,6 +35,11 @@ h2 { margin: 0; font-size: 1.8rem; font-weight: 500; } .detail-notes { max-width: 520px; color: #61625a; font: 1.1rem/1.5 Arial, sans-serif; } .catalog-state { padding: 48px 0; color: #61625a; font: 1rem/1.5 Arial, sans-serif; } .catalog-state--error { color: #a3482a; } +.pagination { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 28px; color: #61625a; font: 0.85rem Arial, sans-serif; } +.pagination button { border: 1px solid #9ca69b; padding: 10px 14px; background: #fffdf8; color: #386052; cursor: pointer; } +.pagination button:hover:not(:disabled) { border-color: #386052; } +.pagination button:focus-visible { outline: 3px solid #a3482a; outline-offset: 3px; } +.pagination button:disabled { cursor: not-allowed; opacity: 0.45; } @media (max-width: 720px) { .catalog-header { padding-top: 56px; } diff --git a/apps/web/src/composables/useCatalogSearch.js b/apps/web/src/composables/useCatalogSearch.js index 844e494..539f5a5 100644 --- a/apps/web/src/composables/useCatalogSearch.js +++ b/apps/web/src/composables/useCatalogSearch.js @@ -2,8 +2,9 @@ import { ref, watch } from 'vue'; import { searchCatalog } from '@/services/catalog-service.js'; -export function useCatalogSearch() { - const query = ref(''); +export function useCatalogSearch({ initialQuery = '', initialPage = 1 } = {}) { + const query = ref(initialQuery); + const page = ref(initialPage); const result = ref({ items: [], pagination: { page: 1, limit: 20, total: 0, totalPages: 0 } }); const isLoading = ref(false); const error = ref(null); @@ -15,7 +16,7 @@ export function useCatalogSearch() { error.value = null; try { - const nextResult = await searchCatalog({ query: query.value }); + const nextResult = await searchCatalog({ query: query.value, page: page.value }); if (currentRequestId === requestId) result.value = nextResult; } catch (requestError) { if (currentRequestId === requestId) error.value = requestError; @@ -25,6 +26,13 @@ export function useCatalogSearch() { } watch(query, load); + watch(page, load); - return { query, result, isLoading, error, load }; + function goToPage(nextPage) { + if (nextPage >= 1 && nextPage <= result.value.pagination.totalPages) { + page.value = nextPage; + } + } + + return { query, page, result, isLoading, error, load, goToPage }; } \ No newline at end of file diff --git a/apps/web/src/pages/CatalogPage.vue b/apps/web/src/pages/CatalogPage.vue index f597d46..5a07d61 100644 --- a/apps/web/src/pages/CatalogPage.vue +++ b/apps/web/src/pages/CatalogPage.vue @@ -1,12 +1,27 @@ \ No newline at end of file diff --git a/apps/web/tests/catalog-service.test.js b/apps/web/tests/catalog-service.test.js index ce4ba7d..b114fa8 100644 --- a/apps/web/tests/catalog-service.test.js +++ b/apps/web/tests/catalog-service.test.js @@ -15,4 +15,19 @@ test('fetches a public listing detail through the API service', async () => { } finally { globalThis.fetch = originalFetch; } +}); + +test('encodes catalog search pagination in the API request', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url) => { + assert.equal(url, 'http://localhost:3000/api/v1/public/search?query=card+set&page=3&limit=20'); + return { ok: true, async json() { return { data: { items: [], pagination: {} } }; } }; + }; + + try { + const { searchCatalog } = await import('../src/services/catalog-service.js'); + await searchCatalog({ query: 'card set', page: 3 }); + } finally { + globalThis.fetch = originalFetch; + } }); \ No newline at end of file diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 5064f86..a1c1a6e 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -98,6 +98,7 @@ docker context use orbstack - The public seller directory is available at `GET /api/v1/public/sellers`; it returns only sellers with visible inventory and exposes display identity without user or contact fields. - The web workspace now has a Vue/Vite public catalog shell with Vue Router, a catalog API service, search composable, loading/error/empty states, and responsive search-first styling. - The web catalog now provides keyboard-accessible listing cards with stable `/listings/:id` links, a public listing detail page, and service-level detail loading/error handling. +- Catalog search and pagination are synchronized to the browser URL (`q` and `page`) with accessible previous/next controls; the web port `5173` is declared in the project port registry. - `ImageStorage`, traversal-safe local storage, multipart upload transport, and binary signature validation are implemented; derivative processing remains. - Derivative orchestration now defines thumbnail, medium, and large WebP storage outputs behind an injected renderer; the concrete worker-backed renderer remains. - The worker owns an image derivative job runner with explicit processing, completion, and failure outcomes; queue transport and persistent job records remain.