From b5670d66e96dcfdc95aaf634f3263d393d980909 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 19:16:27 -0700 Subject: [PATCH 01/16] docs: add AGENTS.md with project architecture and development guidelines --- AGENTS.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bb4fa4c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md + +## Critical Rules + +- **Svelte 5 runes only**: Use `$state`, `$derived`, `$props` - never legacy `$:` or `export let` +- **Context limitation**: `setContext` must be called during component initialization, NOT in `$effect` +- **No custom CSS**: Use TailwindCSS utility classes exclusively +- **TypeScript strict**: All code must be typed, no `any` without justification +- **Package manager**: Use `npm`, not pnpm + +## Non-Obvious Architecture + +### External Data Source + +Brewery data comes from a separate repository: https://github.com/openbrewerydb/openbrewerydb + +- Data contributions go to the dataset repo, NOT this repo +- This repo is the web interface/API wrapper + +### SEO Pattern + +- Use `SEO` component from `src/lib/components/SEO.svelte` +- Use `SEOProvider` for nested SEO configuration +- Context merges parent/child configs via `mergeSEO()` in `src/lib/seo.ts` +- **Important**: `setContext` in SEOProvider must be called synchronously during init, not in effects + +### State Management + +- Stores in `src/lib/stores/` use Svelte 5 runes (e.g., `breweries.svelte.ts`) +- Prefer runes over stores for local component state +- Only use stores when state needs to be shared across unrelated components + +### Type Organization + +- `src/lib/types.ts`: Core types (Brewery, BreweryType, Metadata) +- `src/lib/types/`: Additional type modules (e.g., `metrics.ts`) + +### Data Build Scripts + +Require `GITHUB_TOKEN` environment variable: + +- `npm run data:build` - builds all data +- `npm run authors:build` - GitHub authors +- `npm run changelogs:build` - changelog data +- `npm run contributors:build` - contributor data + +## Environment Variables + +```env +SENTRY_AUTH_TOKEN=your_sentry_token # Optional, for error tracking +GITHUB_TOKEN=your_github_token # Required for data build scripts +``` + +## Deployment + +- Uses Cloudflare Pages/Workers via `@sveltejs/adapter-cloudflare` +- Config: `wrangler.toml`, `svelte.config.js` +- Auto-deploys on push to main via GitHub integration From 502e904fcf424bf07ae5274dc0d7537ab111c5cb Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 19:16:49 -0700 Subject: [PATCH 02/16] feat: add country browsing and redesign browse page with enhanced UI --- src/routes/breweries/browse/+page.svelte | 189 +++++++++++++++++------ src/routes/breweries/browse/+page.ts | 8 + 2 files changed, 147 insertions(+), 50 deletions(-) diff --git a/src/routes/breweries/browse/+page.svelte b/src/routes/breweries/browse/+page.svelte index 6fa44af..1af6fa5 100644 --- a/src/routes/breweries/browse/+page.svelte +++ b/src/routes/breweries/browse/+page.svelte @@ -1,68 +1,157 @@ -
-

Browse Breweries

+
+
+ +
+

+ Explore Breweries +

+

+ Discover breweries around the world through our curated browsing + experience +

+
- {#if error} -
-

Error: {error}

-
- {:else} - -
-

- By State/Province -

- {#if byState.length > 0} -
- {#each byState as item (item.name)} - -

{item.name}

-

{item.count} breweries

-
- {/each} + {#if error} +
+

Error: {error}

+
+ {:else} + +
+
+ +

+ By Country +

- {:else} -

No states available to browse.

- {/if} -
+ {#if byCountry.length > 0} + + {:else} +

+ No countries available to browse. +

+ {/if} +
- -
-

By Type

- {#if byType.length > 0} -
- {#each byType as item (item.name)} - -

{item.name}

-

{item.count} breweries

-
- {/each} + +
+
+ +

+ By State/Province +

- {:else} -

No types available to browse.

- {/if} -
- {/if} + {#if byState.length > 0} +
+ {#each byState as item (item.name)} + +
+

+ {item.name} +

+

+ {item.count.toLocaleString()} +

+
+
+ {/each} +
+ {:else} +

No states available to browse.

+ {/if} +
+ + +
+
+ +

+ By Type +

+
+ {#if byType.length > 0} +
+ {#each byType as item (item.name)} + +
+

+ {item.name} +

+

+ {item.count.toLocaleString()} +

+
+
+ {/each} +
+ {:else} +

No types available to browse.

+ {/if} +
+ {/if} +
diff --git a/src/routes/breweries/browse/+page.ts b/src/routes/breweries/browse/+page.ts index d624aac..cd40762 100644 --- a/src/routes/breweries/browse/+page.ts +++ b/src/routes/breweries/browse/+page.ts @@ -2,6 +2,7 @@ import { API_URL } from '$lib/utils'; interface BreweryMetaResponse { total: string; + by_country: Record; by_state: Record; by_type: Record; } @@ -13,6 +14,7 @@ export async function load() { if (!response.ok) { console.error(`❌ API request failed with status ${response.status}`); return { + byCountry: [], byState: [], byType: [], error: `Failed to fetch metadata with status ${response.status}`, @@ -21,6 +23,10 @@ export async function load() { const data: BreweryMetaResponse = await response.json(); + const byCountry = Object.entries(data.by_country) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => a.name.localeCompare(b.name)); + const byState = Object.entries(data.by_state) .map(([name, count]) => ({ name, count })) .sort((a, b) => a.name.localeCompare(b.name)); @@ -30,12 +36,14 @@ export async function load() { .sort((a, b) => a.name.localeCompare(b.name)); return { + byCountry, byState, byType, }; } catch (error) { console.error('❌ Error fetching brewery metadata:', error); return { + byCountry: [], byState: [], byType: [], error: 'Failed to fetch brewery metadata', From 4ecee1890495bc1d62a1ec5d906ffeabb43e35f7 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 20:13:50 -0700 Subject: [PATCH 03/16] feat: add state-country mapping data for geographic filtering --- src/lib/data/state-country-mapping.json | 190 ++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/lib/data/state-country-mapping.json diff --git a/src/lib/data/state-country-mapping.json b/src/lib/data/state-country-mapping.json new file mode 100644 index 0000000..675afdb --- /dev/null +++ b/src/lib/data/state-country-mapping.json @@ -0,0 +1,190 @@ +{ + "ACT": "Australia", + "Alabama": "United States", + "Bayern": "Germany", + "Aveiro": "Portugal", + "Arizona": "United States", + "Baden-Württemberg": "Germany", + "Argyll": "Scotland", + "Alaska": "United States", + "Bavaria": "Germany", + "Arkansas": "United States", + "British Columbia": "Canada", + "Bouche du Rhône": "France", + "Bolzano": "Italy", + "Bute": "Scotland", + "Beja": "Portugal", + "Brandenburg": "Germany", + "Blekinge": "Sweden", + "Bremen": "Germany", + "Berlin": "Germany", + "Busan": "South Korea", + "California": "United States", + "Central Ostrobothnia": "Finland", + "Colorado": "United States", + "Clare": "Ireland", + "Chungcheongbukdo": "South Korea", + "Carlow": "Ireland", + "Connecticut": "United States", + "Coimbra": "Portugal", + "Chungcheongnamdo": "South Korea", + "Central Finland": "Finland", + "Drenthe": "Netherlands", + "Daejeon": "South Korea", + "Dnipropetrovsk Oblast": "Ukraine", + "Delaware": "United States", + "Cork": "Ireland", + "Dublin": "Ireland", + "East Dunbartonshire": "Scotland", + "Donegal": "Ireland", + "District of Columbia": "United States", + "Deagu": "South Korea", + "Faro": "Portugal", + "Galway": "Ireland", + "Gangwondo": "South Korea", + "Eastern Cape": "South Africa", + "Florida": "United States", + "Gauteng": "South Africa", + "Friesland": "Netherlands", + "East Sussex": "England", + "Free State": "South Africa", + "Flevoland": "Netherlands", + "Gwangju": "South Korea", + "Hawaii": "United States", + "Groningen": "Netherlands", + "Hamburg": "Germany", + "Gyeonggido": "South Korea", + "Gelderland": "Netherlands", + "Georgia": "United States", + "Halland": "Sweden", + "Gyeongsangnamdo": "South Korea", + "Gyeongsangbukdo": "South Korea", + "Illinois": "United States", + "Jeollabukdo": "South Korea", + "Iowa": "United States", + "Indiana": "United States", + "Incheon": "South Korea", + "Jejudo": "South Korea", + "Jeollanamdo": "South Korea", + "Hessen": "Germany", + "Hesse": "Germany", + "Idaho": "United States", + "Kildare": "Ireland", + "KwaZulu-Natal": "South Africa", + "Kansas": "United States", + "K�rnten": "Austria", + "Kymenlaakso": "Finland", + "Kentucky": "United States", + "Kanta-Häme": "Finland", + "Kilkenny": "Ireland", + "Kainuu": "Finland", + "Kerry": "Ireland", + "Louisiana": "United States", + "Longford": "Ireland", + "Lapland": "Finland", + "Laois": "Ireland", + "Limerick": "Ireland", + "Lisboa": "Portugal", + "Limburg": "Netherlands", + "Louth": "Ireland", + "Limpopo": "South Africa", + "Leiria": "Portugal", + "Maine": "United States", + "Massachusetts": "United States", + "Middle": "Isle of Man", + "Lower Saxony": "Germany", + "Michigan": "United States", + "Maryland": "United States", + "Mecklenburg-Vorpommern": "Germany", + "Mayo": "Ireland", + "MIssouri": "United States", + "Meath": "Ireland", + "Mpumalanga": "South Africa", + "Nebraska": "United States", + "Montana": "United States", + "Missouri": "United States", + "Mississippi": "United States", + "NSW": "Australia", + "NT": "Australia", + "SA": "Australia", + "WA": "Australia", + "Monaghan": "Ireland", + "Minnesota": "United States", + "Nevada": "United States", + "Noord-Holland": "Netherlands", + "Noord-Brabant": "Netherlands", + "Nordrhein-Westfalen": "Germany", + "New York": "United States", + "New Mexico": "United States", + "New Jersey": "United States", + "North Carolina": "United States", + "New Hampshire": "United States", + "Niedersachsen": "Germany", + "Nieder�sterreich": "Austria", + "Ohio": "United States", + "Oklahoma": "United States", + "Offaly": "Ireland", + "North Rhine-Westphalia": "Germany", + "Oberösterreich": "Austria", + "North West": "South Africa", + "North Dakota": "United States", + "Ontario": "Canada", + "North Savo": "Finland", + "Northern Ostrobothnia": "Finland", + "Päijät-Häme": "Finland", + "Oregon": "United States", + "Ostrobothnia": "Finland", + "Portalegre": "Portugal", + "Pirkanmaa": "Finland", + "Overijssel": "Netherlands", + "Pennsylvania": "United States", + "Porto": "Portugal", + "QLD": "Australia", + "Osaka": "Japan", + "Saarland": "Germany", + "Rhode Island": "United States", + "Roscommon": "Ireland", + "Rheinland-Pfalz": "Germany", + "Satakunta": "Finland", + "Rhineland-Palatinate": "Germany", + "Salzburg": "Austria", + "Sachsen-Anhalt": "Germany", + "Sachsen": "Germany", + "South Ostrobothnia": "Finland", + "South Carolina": "United States", + "Schleswig-Holstein": "Germany", + "Seoul": "South Korea", + "South Dakota": "United States", + "Singapore": "Singapore", + "South Karelia": "Finland", + "Steiermark": "Austria", + "Sligo": "Ireland", + "South Savo": "Finland", + "Uusimaa": "Finland", + "Tipperary": "Ireland", + "Varsinais-Suomi": "Finland", + "Tennessee": "United States", + "Utrecht": "Netherlands", + "Utah": "United States", + "Thüringen": "Germany", + "TAS": "Australia", + "Texas": "United States", + "VIC": "Australia", + "West Virginia": "United States", + "Western Cape": "South Africa", + "Westmeath": "Ireland", + "Waterford": "Ireland", + "Virginia": "United States", + "Washington": "United States", + "West Sussex": "England", + "Vermont": "United States", + "West Dunbartonshire": "Scotland", + "Wicklow": "Ireland", + "Zuid-Holland": "Netherlands", + "Wyoming": "United States", + "Åland": "Finland", + "dolnośląskie": "Poland", + "Wisconsin": "United States", + "Zeeland": "Netherlands", + "Wexford": "Ireland" +} \ No newline at end of file From 13a9c3101ad630e5a33b30541313c355f888b00a Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 20:21:08 -0700 Subject: [PATCH 04/16] feat: implement smart pagination with ellipses for large page counts --- src/lib/components/SearchPagination.svelte | 80 ++++++++++++++++------ 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/src/lib/components/SearchPagination.svelte b/src/lib/components/SearchPagination.svelte index 51c49e2..ef7f339 100644 --- a/src/lib/components/SearchPagination.svelte +++ b/src/lib/components/SearchPagination.svelte @@ -1,18 +1,10 @@ {#if totalPages > 1} @@ -36,18 +69,23 @@ Previous -
- {#each Array(Math.min(totalPages, maxPagesToShow)) as _item, i (i)} - {@const pageNum = i + 1} - +
+ {#each visiblePages as pageItem, i (i)} + {#if pageItem === 'ellipsis'} + ... + {:else} + + {/if} {/each}
From cd0b1507dc399f025366c3db00d26df585eb85f5 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 20:22:06 -0700 Subject: [PATCH 05/16] feat: implement state and type filtering with improved browse navigation --- src/routes/breweries/+page.svelte | 5 +- src/routes/breweries/+page.ts | 64 ++++++++++++++++++------ src/routes/breweries/browse/+page.svelte | 24 ++++++--- src/routes/breweries/browse/+page.ts | 7 ++- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/routes/breweries/+page.svelte b/src/routes/breweries/+page.svelte index 1a0e3bb..a5ef1b1 100644 --- a/src/routes/breweries/+page.svelte +++ b/src/routes/breweries/+page.svelte @@ -4,6 +4,7 @@ import BrewerySearchForm from '$lib/components/BrewerySearchForm.svelte'; import SearchPagination from '$lib/components/SearchPagination.svelte'; import { goto } from '$app/navigation'; + import { page } from '$app/state'; import SEO from '$lib/components/SEO.svelte'; import { getBreweries, @@ -41,10 +42,8 @@ async function handlePageChange(newPage: number) { if (newPage !== getCurrentPage()) { - const params = new SvelteURLSearchParams(); + const params = new SvelteURLSearchParams(page.url.searchParams); params.set('page', newPage.toString()); - const currentQuery = getSearchQuery(); - if (currentQuery) params.set('query', currentQuery); await goto(`/breweries?${params.toString()}`, { replaceState: true }); } } diff --git a/src/routes/breweries/+page.ts b/src/routes/breweries/+page.ts index 735ffc8..dc85449 100644 --- a/src/routes/breweries/+page.ts +++ b/src/routes/breweries/+page.ts @@ -5,6 +5,8 @@ import type { Brewery, Metadata } from '$lib/types'; export async function load({ url }) { const rawQuery = url.searchParams.get('query'); const query = (rawQuery ?? '').trim(); + const byState = url.searchParams.get('by_state'); + const byType = url.searchParams.get('by_type'); if (rawQuery !== null && query === '') { throw redirect(302, '/breweries'); @@ -13,7 +15,7 @@ export async function load({ url }) { const page = parseInt(url.searchParams.get('page') || '1'); const per_page = parseInt(url.searchParams.get('per_page') || '20'); - if (!query) { + if (!query && !byState && !byType) { return { breweries: [], meta: { @@ -26,8 +28,29 @@ export async function load({ url }) { } try { - const searchUrl = `${API_URL}/breweries/search?query=${encodeURIComponent(query)}&page=${page}&per_page=${per_page}`; - const response = await fetch(searchUrl); + let apiUrl: string; + let metaUrl: string; + + if (query) { + apiUrl = `${API_URL}/breweries/search?query=${encodeURIComponent(query)}&page=${page}&per_page=${per_page}`; + metaUrl = apiUrl; + } else { + apiUrl = `${API_URL}/breweries/?page=${page}&per_page=${per_page}`; + metaUrl = `${API_URL}/breweries/meta?page=${page}&per_page=${per_page}`; + + if (byState) { + apiUrl += `&by_state=${encodeURIComponent(byState)}`; + metaUrl += `&by_state=${encodeURIComponent(byState)}`; + } + + if (byType) { + apiUrl += `&by_type=${encodeURIComponent(byType)}`; + metaUrl += `&by_type=${encodeURIComponent(byType)}`; + } + } + + const response = await fetch(apiUrl); + const metaResponse = await fetch(metaUrl); if (!response.ok) { return { @@ -36,25 +59,36 @@ export async function load({ url }) { total: '0', page: page.toString(), per_page: per_page.toString(), - query, + query: query || '', }, - error: `Search failed with status ${response.status}`, + error: `Request failed with status ${response.status}`, }; } const breweries: Brewery[] = await response.json(); - const total = - breweries.length >= per_page - ? page * per_page + per_page - : (page - 1) * per_page + breweries.length; + let meta: Metadata; + if (query) { + const total = + breweries.length >= per_page + ? page * per_page + per_page + : (page - 1) * per_page + breweries.length; - const meta: Metadata = { - total: total.toString(), - page: page.toString(), - per_page: per_page.toString(), - query: query, - }; + meta = { + total: total.toString(), + page: page.toString(), + per_page: per_page.toString(), + query: query, + }; + } else { + const metaResult = await metaResponse.json(); + meta = { + total: metaResult.total, + page: page.toString(), + per_page: per_page.toString(), + query: query || '', + }; + } return { breweries, diff --git a/src/routes/breweries/browse/+page.svelte b/src/routes/breweries/browse/+page.svelte index 1af6fa5..e63e4fa 100644 --- a/src/routes/breweries/browse/+page.svelte +++ b/src/routes/breweries/browse/+page.svelte @@ -97,7 +97,7 @@ > {#each byState as item (item.name)}
@@ -106,9 +106,13 @@ > {item.name} -

- {item.count.toLocaleString()} -

+
+ +

+ {item.count.toLocaleString()} + {item.count === 1 ? ' brewery' : ' breweries'} +

+
{/each} @@ -132,7 +136,7 @@ > {#each byType as item (item.name)}
@@ -141,9 +145,13 @@ > {item.name} -

- {item.count.toLocaleString()} -

+
+ +

+ {item.count.toLocaleString()} + {item.count === 1 ? ' brewery' : ' breweries'} +

+
{/each} diff --git a/src/routes/breweries/browse/+page.ts b/src/routes/breweries/browse/+page.ts index cd40762..260667a 100644 --- a/src/routes/breweries/browse/+page.ts +++ b/src/routes/breweries/browse/+page.ts @@ -1,4 +1,5 @@ import { API_URL } from '$lib/utils'; +import stateCountryMapping from '$lib/data/state-country-mapping.json'; interface BreweryMetaResponse { total: string; @@ -28,7 +29,11 @@ export async function load() { .sort((a, b) => a.name.localeCompare(b.name)); const byState = Object.entries(data.by_state) - .map(([name, count]) => ({ name, count })) + .map(([name, count]) => ({ + name, + count, + country: stateCountryMapping[name as keyof typeof stateCountryMapping] || 'United States' + })) .sort((a, b) => a.name.localeCompare(b.name)); const byType = Object.entries(data.by_type) From e1bb99c58dc1f44be44af3b801d716f5f2d5756c Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 20:24:40 -0700 Subject: [PATCH 06/16] feat: improve layout alignment for search results and pagination --- src/routes/breweries/+page.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/breweries/+page.svelte b/src/routes/breweries/+page.svelte index a5ef1b1..fae66ad 100644 --- a/src/routes/breweries/+page.svelte +++ b/src/routes/breweries/+page.svelte @@ -77,7 +77,11 @@

Error: {getError()}

{:else if getBreweries().length > 0} -
+
{#if getSearchQuery()}

@@ -122,7 +126,7 @@

-
+
Date: Mon, 1 Jun 2026 20:29:06 -0700 Subject: [PATCH 07/16] feat: add active filter badges with removal functionality to search results --- src/routes/breweries/+page.svelte | 74 +++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/routes/breweries/+page.svelte b/src/routes/breweries/+page.svelte index fae66ad..13a8c0f 100644 --- a/src/routes/breweries/+page.svelte +++ b/src/routes/breweries/+page.svelte @@ -47,6 +47,13 @@ await goto(`/breweries?${params.toString()}`, { replaceState: true }); } } + + function removeParam(param: string): string { + const params = new SvelteURLSearchParams(page.url.searchParams); + params.delete(param); + params.delete('page'); + return params.toString(); + }

Search Breweries

-
+
+ {#if !getLoading() && !getError() && (page.url.searchParams.get('query') || page.url.searchParams.get('by_state') || page.url.searchParams.get('by_type'))} +
+ Active Filters: + {#if page.url.searchParams.get('query')} + + Query: "{page.url.searchParams.get('query')}" + × + + {/if} + {#if page.url.searchParams.get('by_state')} + + State: {page.url.searchParams.get('by_state')} + × + + {/if} + {#if page.url.searchParams.get('by_type')} + + Type: {page.url.searchParams.get('by_type')} + × + + {/if} + Clear All +
+ {/if} + {#if getLoading()}
{:else if getBreweries().length > 0}
- {#if getSearchQuery()} -
-

- {(getCurrentPage() - 1) * getItemsPerPage() + 1} + {#if getTotalBreweries() > 0} +

+

+ Showing {(getCurrentPage() - 1) * getItemsPerPage() + 1} - {Math.min( getCurrentPage() * getItemsPerPage(), getTotalBreweries() )} - of {getTotalBreweries()} breweries (page {getCurrentPage()} of {getTotalPages()}) + of {getTotalBreweries().toLocaleString()} breweries (page {getCurrentPage()} + of {getTotalPages()})

{/if} From 6d08225b76be5adb19e120ccbd6b5e3e74bef067 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 20:33:19 -0700 Subject: [PATCH 08/16] chore: add trailing newline and format state-country mapping code --- src/lib/data/state-country-mapping.json | 2 +- src/routes/breweries/browse/+page.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/data/state-country-mapping.json b/src/lib/data/state-country-mapping.json index 675afdb..db2de9f 100644 --- a/src/lib/data/state-country-mapping.json +++ b/src/lib/data/state-country-mapping.json @@ -187,4 +187,4 @@ "Wisconsin": "United States", "Zeeland": "Netherlands", "Wexford": "Ireland" -} \ No newline at end of file +} diff --git a/src/routes/breweries/browse/+page.ts b/src/routes/breweries/browse/+page.ts index 260667a..3e36994 100644 --- a/src/routes/breweries/browse/+page.ts +++ b/src/routes/breweries/browse/+page.ts @@ -32,7 +32,9 @@ export async function load() { .map(([name, count]) => ({ name, count, - country: stateCountryMapping[name as keyof typeof stateCountryMapping] || 'United States' + country: + stateCountryMapping[name as keyof typeof stateCountryMapping] || + 'United States', })) .sort((a, b) => a.name.localeCompare(b.name)); From 31565097c1faff00926ff608aec9ce0263d67cef Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 21:11:14 -0700 Subject: [PATCH 09/16] refactor: consolidate pagination components and enhance page navigation controls --- src/lib/components/Pagination.svelte | 121 +++++++++++++++++---- src/lib/components/SearchPagination.svelte | 100 ----------------- src/routes/breweries/+page.svelte | 30 ++--- 3 files changed, 118 insertions(+), 133 deletions(-) delete mode 100644 src/lib/components/SearchPagination.svelte diff --git a/src/lib/components/Pagination.svelte b/src/lib/components/Pagination.svelte index 71a36df..e5adbba 100644 --- a/src/lib/components/Pagination.svelte +++ b/src/lib/components/Pagination.svelte @@ -21,17 +21,21 @@ let totalPages = $derived(Math.ceil(+meta.total / +meta.per_page)); function handlePageChange(targetPage: number) { - if (onPageChange) { + if ( + onPageChange && + targetPage !== page && + targetPage >= 1 && + targetPage <= totalPages + ) { onPageChange(targetPage); } } const getPageUrl = (targetPage: number): string => { - // For search context, we don't generate URLs since we use the callback if (context === 'search') { return '#'; } - + let baseUrl = `/breweries/${country}`; if (context === 'state' || context === 'city') { @@ -50,48 +54,125 @@ return baseUrl; }; + + // Calculate the array of pages to display with ellipses + let visiblePages = $derived.by(() => { + const pages: (number | 'ellipsis')[] = []; + const maxVisible = 5; // Number of pages to show in the sliding window + + if (totalPages <= maxVisible + 4) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always include page 1 + pages.push(1); + + let start = Math.max(2, page - 2); + let end = Math.min(totalPages - 1, page + 2); + + if (page <= 3) { + end = 1 + maxVisible; + } else if (page >= totalPages - 2) { + start = totalPages - maxVisible; + } + + if (start > 2) { + pages.push('ellipsis'); + } + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + if (end < totalPages - 1) { + pages.push('ellipsis'); + } + + // Always include last page + pages.push(totalPages); + } + + return pages; + }); -
- {#if page > 1} +{#if totalPages > 1} +
+ {#if context === 'search' && onPageChange} {:else} Previous {/if} - {/if} - - Page {page} of {totalPages} - + +
+ {#each visiblePages as pageItem, i (i)} + {#if pageItem === 'ellipsis'} + ... + {:else if context === 'search' && onPageChange} + + {:else} + + {pageItem} + + {/if} + {/each} +
- {#if page < totalPages} + {#if context === 'search' && onPageChange} {:else} = totalPages ? '#' : getPageUrl(page + 1)} > Next {/if} - {/if} -
+
+{/if} diff --git a/src/lib/components/SearchPagination.svelte b/src/lib/components/SearchPagination.svelte deleted file mode 100644 index ef7f339..0000000 --- a/src/lib/components/SearchPagination.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - -{#if totalPages > 1} -
- - -
- {#each visiblePages as pageItem, i (i)} - {#if pageItem === 'ellipsis'} - ... - {:else} - - {/if} - {/each} -
- - -
-{/if} diff --git a/src/routes/breweries/+page.svelte b/src/routes/breweries/+page.svelte index 13a8c0f..ab0ba46 100644 --- a/src/routes/breweries/+page.svelte +++ b/src/routes/breweries/+page.svelte @@ -2,7 +2,7 @@ import BreweriesTable from '$lib/components/BreweriesTable.svelte'; import BreweryCard from '$lib/components/BreweryCard.svelte'; import BrewerySearchForm from '$lib/components/BrewerySearchForm.svelte'; - import SearchPagination from '$lib/components/SearchPagination.svelte'; + import Pagination from '$lib/components/Pagination.svelte'; import { goto } from '$app/navigation'; import { page } from '$app/state'; import SEO from '$lib/components/SEO.svelte'; @@ -12,8 +12,6 @@ getError, resetSearch, initializeStore, - getHasNextPage, - getHasPreviousPage, getCurrentPage, getItemsPerPage, getTotalBreweries, @@ -151,11 +149,14 @@
{/if} -
@@ -183,11 +184,14 @@
-
From 32f18488a2f5c91ff70e28f74f12f5efa81230f2 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 21:23:42 -0700 Subject: [PATCH 10/16] feat: implement client-side pagination for search results with batch fetching --- src/routes/breweries/+page.ts | 102 +++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/src/routes/breweries/+page.ts b/src/routes/breweries/+page.ts index dc85449..ee0d0b9 100644 --- a/src/routes/breweries/+page.ts +++ b/src/routes/breweries/+page.ts @@ -28,15 +28,56 @@ export async function load({ url }) { } try { - let apiUrl: string; - let metaUrl: string; + let breweries: Brewery[] = []; + let meta: Metadata; if (query) { - apiUrl = `${API_URL}/breweries/search?query=${encodeURIComponent(query)}&page=${page}&per_page=${per_page}`; - metaUrl = apiUrl; + // Calculate start index and map it to API batch page number of size 200 + const svelteStart = (page - 1) * per_page; + const apiPage = Math.floor(svelteStart / 200) + 1; + const localOffset = svelteStart % 200; + + const apiUrl = `${API_URL}/breweries/search?query=${encodeURIComponent(query)}&page=${apiPage}&per_page=200`; + const response = await fetch(apiUrl); + + if (!response.ok) { + return { + breweries: [], + meta: { + total: '0', + page: page.toString(), + per_page: per_page.toString(), + query: query, + }, + error: `Request failed with status ${response.status}`, + }; + } + + const apiBreweries: Brewery[] = await response.json(); + breweries = apiBreweries.slice(localOffset, localOffset + per_page); + + let total: number; + if (apiBreweries.length < 200) { + // We reached the actual end of results + total = (apiPage - 1) * 200 + apiBreweries.length; + } else { + // We got exactly 200 results, so there might be more beyond this batch. + // We do progressive discovery beyond the currently fetched batch size. + total = (apiPage - 1) * 200 + apiBreweries.length; + if (localOffset + per_page >= apiBreweries.length) { + total += per_page; + } + } + + meta = { + total: total.toString(), + page: page.toString(), + per_page: per_page.toString(), + query: query, + }; } else { - apiUrl = `${API_URL}/breweries/?page=${page}&per_page=${per_page}`; - metaUrl = `${API_URL}/breweries/meta?page=${page}&per_page=${per_page}`; + let apiUrl = `${API_URL}/breweries/?page=${page}&per_page=${per_page}`; + let metaUrl = `${API_URL}/breweries/meta?page=${page}&per_page=${per_page}`; if (byState) { apiUrl += `&by_state=${encodeURIComponent(byState)}`; @@ -47,46 +88,31 @@ export async function load({ url }) { apiUrl += `&by_type=${encodeURIComponent(byType)}`; metaUrl += `&by_type=${encodeURIComponent(byType)}`; } - } - const response = await fetch(apiUrl); - const metaResponse = await fetch(metaUrl); - - if (!response.ok) { - return { - breweries: [], - meta: { - total: '0', - page: page.toString(), - per_page: per_page.toString(), - query: query || '', - }, - error: `Request failed with status ${response.status}`, - }; - } + const response = await fetch(apiUrl); + const metaResponse = await fetch(metaUrl); - const breweries: Brewery[] = await response.json(); - - let meta: Metadata; - if (query) { - const total = - breweries.length >= per_page - ? page * per_page + per_page - : (page - 1) * per_page + breweries.length; + if (!response.ok) { + return { + breweries: [], + meta: { + total: '0', + page: page.toString(), + per_page: per_page.toString(), + query: '', + }, + error: `Request failed with status ${response.status}`, + }; + } - meta = { - total: total.toString(), - page: page.toString(), - per_page: per_page.toString(), - query: query, - }; - } else { + breweries = await response.json(); const metaResult = await metaResponse.json(); + meta = { total: metaResult.total, page: page.toString(), per_page: per_page.toString(), - query: query || '', + query: '', }; } From 391ac1dc3f636cfeaa606f47d915cb36d9a06937 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 21:42:41 -0700 Subject: [PATCH 11/16] feat: add mobile-responsive pagination layout with simplified controls --- src/lib/components/Pagination.svelte | 50 +++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/lib/components/Pagination.svelte b/src/lib/components/Pagination.svelte index e5adbba..055ee22 100644 --- a/src/lib/components/Pagination.svelte +++ b/src/lib/components/Pagination.svelte @@ -98,8 +98,56 @@ {#if totalPages > 1} + +
+ {#if context === 'search' && onPageChange} + + {:else} + + Previous + + {/if} + + + Page {page} of {totalPages} + + + {#if context === 'search' && onPageChange} + + {:else} + = totalPages ? '#' : getPageUrl(page + 1)} + > + Next + + {/if} +
+ +
{#if context === 'search' && onPageChange} From 73ecf919d8135b3c919bf644ef1d633fb4549290 Mon Sep 17 00:00:00 2001 From: Chris J Mears Date: Mon, 1 Jun 2026 21:54:17 -0700 Subject: [PATCH 12/16] feat: enhance map display with proper zoom level and coordinate bounds --- src/routes/b/[id]/+page.svelte | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/routes/b/[id]/+page.svelte b/src/routes/b/[id]/+page.svelte index 605efb9..646cad5 100644 --- a/src/routes/b/[id]/+page.svelte +++ b/src/routes/b/[id]/+page.svelte @@ -168,7 +168,14 @@