Skip to content

Group Saba venue localities under one island heading (#116) - #120

Merged
spizeck merged 2 commits into
mainfrom
fix/issue-116-saba-venue-grouping
Sep 23, 2026
Merged

spizeck merged 2 commits into
mainfrom
fix/issue-116-saba-venue-grouping

Conversation

@spizeck

@spizeck spizeck commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

/where-to-buy fragmented Saba venues into multiple island groups. islandKey() canonicalized SXM/Statia spelling variants but lowercased every other locationName as-is, so production values like Fort Bay, Saba, Windwardside, Saba, and Windwardside / The Bottom, Saba each became a distinct "island" — rendered by islandDisplayName() as headings like Fort bay, saba — and the Island filter offered several choices that all meant Saba.

Fix per Issue #116 Option 1 (canonical island-level grouping): the island is the final comma-separated segment of locationName. Any <locality>, Saba value resolves to the canonical key saba; matching the whole segment (not a bare saba substring) avoids misclassifying unrelated future values like Sabana. Known keys now have intentional labels via ISLAND_DISPLAY_NAMES (saba → Saba, SXM/Statia labels unchanged); the fallback for unknown islands title-cases each word rather than only the first letter.

Grouping and filtering changed — venue data did not: each card still renders the raw locationName (e.g. Windwardside / The Bottom, Saba). No Firestore writes, no data migration.

Closes #116

Changes

  • lib/venue-filters.ts: islandKey() maps the <locality>, Saba suffix to "saba" (SXm/Statia paths and the empty→saba default unchanged); islandDisplayName() uses an explicit known-island label map plus a word-wise title-case fallback.
  • lib/where-to-buy-fixture.ts: the three Saba fixture venues now use distinct real-world localities (Fort Bay, Saba, Windwardside, Saba, Windwardside / The Bottom, Saba) — the previous uniform Saba values could never expose this bug.
  • tests/lib/venue-filters.test.ts: +5 tests — every Saba variant resolves to "saba", substring-safety (Sabana Grande), canonical display label via islandDisplayName(islandKey(...)), one saba option/group for mixed localities, and island: "saba" filtering returning all Saba venues.
  • smoke-tests/where-to-buy-filters.spec.ts: island select now asserted to contain exactly All islands / Saba / Sint Maarten / Saint Martin / SXM; new regression test proves one Saba h2, no Fort bay, saba/Windwardside, saba headings, all 3 Saba cards under the Saba section, raw locality text still on cards, and the saba filter returning all three.
  • docs/admin/managing-locations.md: grouping rules now document the <locality>, Saba convention.

Verification

  • npm ci
  • npm run check:react-versions
  • npx tsc --noEmit
  • npm run lint
  • npm test — 334 tests / 101 suites pass
  • npm run test:rules — 29 tests / 7 suites pass
  • npm run build
  • npm run test:smoke — 117 Playwright tests pass, including the new Where to Buy fragments Saba venues into multiple sections with lowercase headings #116 regression test
  • npm run check:md-links
  • npm audit --omit=dev — 0 vulnerabilities
  • Regression proof: the new tests + updated fixture produce 6 failures against the old islandKey/islandDisplayName (verified by running the suite against the main version of lib/venue-filters.ts)
  • Real-data check: built locally with real config — production's 11 venues across 4 distinct Saba locality strings render under a single Saba heading at desktop and mobile widths, localities intact on cards

Risk / deployment notes

  • Customer-facing change: Saba group headings normalize to Saba; the Island filter collapses duplicate Saba options into one. When only Saba venues are live (as in current production data) the Island select stays hidden by the existing islands.length > 1 rule.
  • No schema/data changes; no new dependencies; analytics events unchanged.
  • No secrets, credentials, or private data were committed.

Generated with Devin

Summary by Sourcery

Consolidate locality-qualified Saba venues into one canonical island group while preserving their displayed location details.

Bug Fixes:

  • Group Saba venues with locality-qualified location names under a single canonical Saba island heading and filter option.
  • Prevent location names that merely contain the Saba substring from being classified as Saba.

Enhancements:

  • Use explicit labels for known islands and word-wise title casing for unknown island headings.
  • Preserve locality-qualified location names on venue cards while consolidating their island grouping.

Documentation:

  • Document the , Saba location-name convention and its grouping behavior for administrators.

Tests:

  • Add unit and smoke coverage for Saba locality grouping, filtering, display labels, and substring safety.

Chores:

  • Update fixture venues with distinct Saba locality values to exercise the grouping regression.

islandKey() canonicalized SXM and Statia spelling variants but returned
any other locationName lowercased as-is, so Saba localities like
"Fort Bay, Saba" or "Windwardside / The Bottom, Saba" each produced a
separate island group — and islandDisplayName() then rendered those keys
as headings like "Fort bay, saba". The Island filter exposed several
options that all meant Saba.

The island is now the final comma-separated segment of locationName:
"<locality>, Saba" resolves to the canonical "saba" key without relying
on a bare substring match, so values that merely contain "saba" are not
misclassified. Known keys get intentional display labels via
ISLAND_DISPLAY_NAMES; unknown future keys title-case word by word
instead of only capitalizing the first letter. The raw locationName
still renders on each venue card — grouping changed, data did not.

The /where-to-buy-fixture records now use distinct Saba localities so
the Playwright suite covers this regression (uniform "Saba" values could
never have exposed it), and new unit/smoke tests pin the one-option,
one-heading, all-venues-under-Saba contract.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes fragmented Saba venue groups by deriving the island from the final comma-separated location segment, adds explicit canonical labels and safe fallback formatting, updates fixtures to exercise real locality variants, and adds unit, smoke-test, and documentation coverage without changing venue data schemas or persistence.

Flow diagram for canonical Saba venue grouping

flowchart LR
    A["Raw locationName"] --> B["Normalize location"]
    B --> C{"Final comma-separated segment is saba?"}
    C -->|Yes| D["Canonical key: saba"]
    C -->|No| E["Existing SXM/Statia rules or fallback key"]
    D --> F["Display heading: Saba"]
    D --> G["Venue card keeps raw locality"]
Loading

File-Level Changes

Change Details Files
Canonicalize Saba localities to a single island key and provide stable display labels.
  • Treat the final comma-separated segment Saba as the canonical saba key while preserving existing SXM, Statia, and empty-location behavior.
  • Use explicit labels for known islands and word-wise title casing for unknown keys.
  • Retain raw locality values for venue-card display while grouping and filtering by canonical key.
lib/venue-filters.ts
Expand fixture data to reproduce locality-based Saba fragmentation.
  • Replace uniform Saba fixture locations with Fort Bay, Windwardside, and Windwardside / The Bottom locality values.
lib/where-to-buy-fixture.ts
Add unit coverage for canonicalization, display naming, grouping, and filtering.
  • Verify Saba variants normalize to one key and substring lookalikes such as Sabana Grande do not.
  • Verify one Saba option/group includes all Saba-locality venues and filtering returns all of them.
  • Verify known labels and word-wise fallback formatting.
tests/lib/venue-filters.test.ts
Add end-to-end regression coverage for the customer-facing venue directory.
  • Assert the island selector exposes one Saba option and the expected island labels.
  • Assert one Saba section contains all three venues, raw locality text remains visible, and the Saba filter returns all three.
smoke-tests/where-to-buy-filters.spec.ts
Document the supported locality-plus-island naming convention for administrators.
  • Explain that <locality>, Saba groups under Saba while retaining locality text on cards.
  • Document word-wise capitalization for other island values.
docs/admin/managing-locations.md

Assessment against linked issues

Issue Objective Addressed Explanation
#116 Group all Saba venues, including locality-qualified names such as "Fort Bay, Saba" and "Windwardside, Saba", under one canonical Saba island group and provide a single Saba filter option. ✅
#116 Render the Saba section heading with correct capitalization while preserving each venue's raw locality name on its venue card. ✅
#116 Add regression coverage and documentation for Saba locality normalization, display-name casing, grouping/filtering behavior, and preservation of locality details. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
deepdivebrewing-web Ready Ready Preview Sep 23, 2026 11:00pm UTC

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/venue-filters.ts" line_range="99-103" />
<code_context>
-  if (key === "statia") return "Sint Eustatius / Statia";
-  return key.charAt(0).toUpperCase() + key.slice(1);
+  return (
+    ISLAND_DISPLAY_NAMES[key] ??
+    key.replace(/\b[a-z]/g, (ch) => ch.toUpperCase())
+  );
 }
</code_context>
<issue_to_address>
**issue (bug_risk):** `islandDisplayName()` returns inherited Object prototype properties instead of a string for unknown keys such as `constructor`, `toString`, or `__proto__`. A venue with one of those location names then causes `distinctIslands()` or `groupVenuesByIsland()` to call `.localeCompare()` on a non-string and crash.

**Triggers:** When a venue has a location name that lowercases to `constructor`, `toString`, or `__proto__`.

**Suggested fix:** Use an object with a null prototype or guard the lookup with `Object.hasOwn(ISLAND_DISPLAY_NAMES, key)` before returning the mapped label.

```suggestion
const ISLAND_DISPLAY_NAMES: Record<string, string> = Object.assign(
  Object.create(null),
  {
    saba: "Saba",
    sxm: "Sint Maarten / Saint Martin / SXM",
    statia: "Sint Eustatius / Statia",
  }
);
```
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: lib/venue-filters.ts:103


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/venue-filters.ts Outdated
A plain-object lookup returns inherited Object.prototype members for
keys like "constructor" or "__proto__", which would surface a non-string
from islandDisplayName() and crash the localeCompare ordering. Addresses
a Sourcery review finding on the PR.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@spizeck
spizeck merged commit 8c5428f into main Sep 23, 2026
4 checks passed
@spizeck
spizeck deleted the fix/issue-116-saba-venue-grouping branch September 23, 2026 23:10

This branch was successfully deployed

1 active deployment
Preview — 95460610 Deployed Sep 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Where to Buy fragments Saba venues into multiple sections with lowercase headings

1 participant