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 @@
@@ -33,6 +48,24 @@ onMounted(load);
+
+
\ 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.