Skip to content

fix(merchant): make map deep link select the merchant #966#1047

Merged
escapedcat merged 6 commits into
mainfrom
fix/view-on-map-merchant-deeplink
Jun 5, 2026
Merged

fix(merchant): make map deep link select the merchant #966#1047
escapedcat merged 6 commits into
mainfrom
fix/view-on-map-merchant-deeplink

Conversation

@escapedcat

@escapedcat escapedcat commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
image

Started as a fix for the merchant page's "View on main map" deep link, then grew to cover
the selected-pin highlight and clustering.

  • Deep link (Social icons #966): the link put the merchant in the URL hash, but the map reads it
    from the query string (since fix(map): serve merchant OG image for ?merchant= URL param #1019), so nothing got selected. Extracted
    buildMerchantMapHref() as the canonical /map?merchant=…#zoom/lat/lng link (used by
    MerchantHero), refreshed the stale mapHash.ts comment, and url-encoded the id.
  • Selected-pin highlight (feat(map): switch to maplibre native #1003): restored the lost selection cue as a "pulsing
    locator" — a CSS-animated ring + dot in the pin's hue; honors prefers-reduced-motion.
    Removed the orphaned Leaflet-era glow CSS.
  • Clustering (feat(map): switch to maplibre native #1003): selecting a merchant zooms past the cluster threshold to reveal
    its pin, and the pulse hides when the pin is rolled into a cluster (returning when it
    declusters).

Tests: deep-link round-trip + a regression test for the legacy hash format.

Summary by CodeRabbit

  • New Features
    • Standardized merchant deep-links: merchant ID now in the query string and viewport in
      the URL hash; merchant card “View on main map” uses the canonical link.
    • New selected-merchant visual: a CSS-driven pulsing overlay; map now zooms in to reveal
      an individual pin when deep-linking/selecting.

Summary by CodeRabbit

  • New Features

    • Canonical merchant deep-links: viewport encoded in the URL hash; merchant ID moved to the query string. Merchant card “View on main map” uses this canonical link.
    • Improved selected-merchant visuals: CSS-driven pulsing overlay and behavior that zooms to reveal individual pins when needed.
  • Tests

    • Added deep-link round-trip tests and a regression test for legacy hash formats.

…966

The hero's "View on main map" link encoded the merchant in the URL hash
(/map#18/lat/lng&merchant=123), but since #1019 ("switch merchant URL from
hash to query params") the map reads the merchant only from the query string
via parseMerchantHash(). With an empty query string the parser bailed out, so
the map opened at the right viewport but never selected the marker or opened
the drawer.

- Extract buildMerchantMapHref() as the single source of truth for the
  /map?merchant=…#zoom/lat/lng deep link, used by MerchantHero
- Refresh the stale mapHash.ts header comment (merchant/view live in the query
  string, not the hash)
- Add a round-trip regression test: the link the helper builds must resolve
  back through parseMerchantHash + parseHashCoords (proven to fail on the old
  hash format)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@netlify

netlify Bot commented Jun 5, 2026

Copy link
Copy Markdown

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit 5dcf55c
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/6a22c7c6c6e3190008de6d48
😎 Deploy Preview https://deploy-preview-1047--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 52 (🔴 down 34 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 96 (no change from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 123c7ef6-eec6-4f58-a241-8b70872456f7

📥 Commits

Reviewing files that changed from the base of the PR and between f2908a3 and 5dcf55c.

📒 Files selected for processing (1)
  • src/routes/map/+page.svelte
💤 Files with no reviewable changes (1)
  • src/routes/map/+page.svelte

📝 Walkthrough

Walkthrough

Standardizes merchant deep links (merchant in query, viewport in hash), adds buildMerchantMapHref and tests, updates MerchantHero to use it, introduces CSS pulse overlay, and implements map-page reveal, pulse Marker, lifted pin behavior, and cleanup.

Changes

Merchant Deep-Link URL Format Standardization & Selected-Pin Pulse

Layer / File(s) Summary
URL format contract and link builder
src/lib/map/mapHash.ts, src/lib/merchantDrawerHash.ts
Map hash docs clarify viewport-only hash (#zoom/lat/lng, optionally /bearing/pitch). New buildMerchantMapHref() writes merchant to the query string and zoom/lat/lng to the hash fragment.
Deep-link builder testing and validation
src/lib/merchantDrawerHash.test.ts
Tests import buildMerchantMapHref and parseHashCoords, validating round-trip parsing, merchant-in-query encoding, and regression for legacy &merchant= hash format.
Merchant card link integration
src/routes/merchant/[id]/components/MerchantHero.svelte
MerchantHero replaces hardcoded /map#...&merchant=... assembly with buildMerchantMapHref(id, lat, long).
Selected-merchant pulse styles
src/app.css
Adds .bm-selected-pulse, .bm-pulse-ring, .bm-pulse-dot and @keyframes bm-pulse; provides --bm-pulse-color and reduced-motion handling.
Map page: pulse marker, reveal zoom, and selection wiring
src/routes/map/+page.svelte
Adds REVEAL_ZOOM, dynamic maplibre-gl namespace storage, pulse Marker DOM creation/removal, logic to lift selected pin (icon-size), pan/zoom-to-reveal behavior, idle-based pulse visibility updates, reactive sync on merchant selection, and onDestroy cleanup.
sequenceDiagram
  participant Client
  participant Router
  participant MapPage
  participant parseMerchantHash
  participant maplibre_Marker
  Client->>Router: click /map?merchant=...#zoom/lat/lng
  Router->>MapPage: navigate to /map with URL
  MapPage->>parseMerchantHash: parse merchant from query + hash coords
  MapPage->>MapPage: pan/zoom to REVEAL_ZOOM (if needed)
  MapPage->>maplibre_Marker: create/update pulse Marker at coords
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • teambtcmap/btcmap.org#1019: Both PRs revolve around merchantDrawerHash URL handling by encoding the merchant ID in the query string and related drawer/link parsing.
  • teambtcmap/btcmap.org#819: Overlaps on map page deep-link handling and parseMerchantHash/reveal behavior.

Suggested labels

Review effort 2/5

Suggested reviewers

  • bubelov
  • dadofsambonzuki

Poem

🐰 I stitched a link with tidy thread, merchant in query, coords ahead,
A pulsing dot to show the way, unclustered bright at reveal-of-day,
Tests confirm the round-trip true, old hashes left but parsed for view,
Hop, click, and pan — the map’s in tune, a rabbit cheers beneath the moon 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main fix: making the merchant map deep link select the merchant, directly referencing issue #966.
Description check ✅ Passed The PR description includes all key sections from the template (issue reference, detailed changes, images) and provides comprehensive context about the fixes (deep link, selected-pin highlight, clustering) with test coverage noted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/view-on-map-merchant-deeplink

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/merchantDrawerHash.ts`:
- Around line 78-85: The buildMerchantMapHref function currently injects
merchantId raw into the query string which can break URLs if it contains
reserved characters; update buildMerchantMapHref to URL-encode merchantId (e.g.,
call encodeURIComponent(String(merchantId))) when interpolating the query param
so the returned string is safe and cannot corrupt the hash/fragment
(`#zoom/lat/lng`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 025e5544-5f97-4056-85a9-e2e6f70fc466

📥 Commits

Reviewing files that changed from the base of the PR and between 94bfea5 and 97958c3.

📒 Files selected for processing (4)
  • src/lib/map/mapHash.ts
  • src/lib/merchantDrawerHash.test.ts
  • src/lib/merchantDrawerHash.ts
  • src/routes/merchant/[id]/components/MerchantHero.svelte

Comment thread src/lib/merchantDrawerHash.ts
@dadofsambonzuki

Copy link
Copy Markdown
Member

Good catch!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes the merchant page’s “View on main map” deep link so it selects/open the correct merchant drawer after the #1019 switch from hash-based merchant IDs to query-string merchant IDs.

Changes:

  • Replaces the merchant hero map link with a new buildMerchantMapHref() helper that encodes merchant in the query string and viewport in the hash.
  • Updates mapHash.ts header documentation to reflect that only viewport data lives in the hash.
  • Adds a regression/round-trip test ensuring the generated link is readable by both parseMerchantHash() and parseHashCoords().

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/routes/merchant/[id]/components/MerchantHero.svelte Uses the new canonical helper to build the “View on main map” deep link.
src/lib/merchantDrawerHash.ts Adds buildMerchantMapHref() as the single source of truth for merchant map deep links.
src/lib/merchantDrawerHash.test.ts Adds round-trip and regression tests for the new deep-link format.
src/lib/map/mapHash.ts Updates header comment to document query-vs-hash responsibilities post-#1019.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/merchantDrawerHash.ts
escapedcat and others added 3 commits June 5, 2026 13:29
Implements design C ("centered pulsing locator") from the Claude Design
handoff. Selecting a pin scales it up and drops a DOM overlay marker on its
geo point: a pulsing ring + a center dot in the pin's own hue (teal / bitcoin
orange). The pulse is pure CSS keyframes (composited, honors
prefers-reduced-motion); the GL pin layer is untouched apart from the
icon-size lift.

- maplibre is imported dynamically, so the namespace is stashed in onMount to
  build the Marker reactively
- the pulse repositions on selection change and on $places (so a deep-linked
  merchant gets its pulse once places sync in); removed on deselect + onDestroy
- replaces the orphaned Leaflet-era .selected-marker glow CSS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ster state #1003

The locator pulse is pinned to the merchant's coordinate regardless of
clustering, so it floated orphaned whenever no individual pin was there to sit
on. Two fixes:

Reveal on selection — zoom past CLUSTERING_DISABLED_ZOOM (new REVEAL_ZOOM 17.5)
on every selection path that could otherwise land clustered:
- panToNearbyMerchant (nearby list) — was pan-only
- panToPlace (deep-link pan) — was landing on the still-clustered
  DEFAULT_MAP_ZOOM (15)
- the deep-link reveal now also fires when the URL's hash zoom is below the
  threshold (e.g. ?merchant=X#14/…); a hash zoom at/above 17 is honoured
Search-result selection already zoomed past 17.

Hide on re-cluster — when the user manually zooms out and the merchant is
rolled back into a cluster, hide the pulse (no pin to highlight) and show it
again once the pin renders individually. Checked against the live source
cluster state on moveend/idle, since whether a point clusters depends on its
neighbours, not the zoom level alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1003

Code-review follow-ups:
- syncSelectionPulse no longer early-returns for an unchanged selection, so
  boosting the selected merchant from the drawer (updateSinglePlace → $places)
  recolours the pulse teal → orange to match the now-orange pin. The
  colour/position refresh is idempotent on an already-added marker; the
  cluster-visibility query stays gated behind a real selection change.
- pulse colours now come from PIN_FILL_REGULAR / PIN_FILL_BOOSTED (the GL pin
  sprite's source of truth) instead of duplicated literals, so they can't
  desync; the CSS fallback is documented as mirroring PIN_FILL_REGULAR.
- dropped the redundant updatePulseVisibility() call on moveend; the idle
  handler is the authoritative reconcile point (avoids a premature/duplicate
  querySourceFeatures per interaction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/app.css (1)

180-191: ⚡ Quick win

Consider using transform: scale() instead of animating width/height for better performance.

Animating width and height triggers layout recalculation on every frame. Since the comment mentions the pulse is "pure CSS (composited)", using transform: scale() would actually achieve compositor-only animation.

♻️ Proposed refactor using transform: scale()
+	.bm-pulse-ring {
+		width: 70px;
+		height: 70px;
+		border: 3px solid var(--bm-pulse-color, `#0e95af`);
+		opacity: 0;
+		animation: bm-pulse 1.8s ease-out infinite;
+	}
+
 	`@keyframes` bm-pulse {
 		0% {
-			width: 18px;
-			height: 18px;
+			transform: translate(-50%, -50%) scale(0.257);
 			opacity: 0.55;
 		}
 		100% {
-			width: 70px;
-			height: 70px;
+			transform: translate(-50%, -50%) scale(1);
 			opacity: 0;
 		}
 	}

Note: The translate(-50%, -50%) must be included in the keyframes since transform replaces the base transform entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.css` around lines 180 - 191, The keyframes named bm-pulse currently
animates width/height which causes layout thrashing; update the `@keyframes`
bm-pulse to animate transform: scale(...) (keeping opacity) instead of
width/height and ensure you include the element's translate(-50%, -50%) in the
keyframes because transform replaces the base transform; adjust the element's
base width/height (the original 18px start size) in the static CSS so the visual
sizes match the previous animation and keep the 0%/100% opacity values
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/app.css`:
- Around line 180-191: The keyframes named bm-pulse currently animates
width/height which causes layout thrashing; update the `@keyframes` bm-pulse to
animate transform: scale(...) (keeping opacity) instead of width/height and
ensure you include the element's translate(-50%, -50%) in the keyframes because
transform replaces the base transform; adjust the element's base width/height
(the original 18px start size) in the static CSS so the visual sizes match the
previous animation and keep the 0%/100% opacity values unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c03bb95-6736-49a6-90ff-8a66555689f3

📥 Commits

Reviewing files that changed from the base of the PR and between 97958c3 and 43ce338.

📒 Files selected for processing (2)
  • src/app.css
  • src/routes/map/+page.svelte

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/lib/merchantDrawerHash.ts
escapedcat and others added 2 commits June 5, 2026 14:34
PR #1047 review (Copilot): the id was interpolated raw into the query string,
so a non-numeric/crafted id could split into extra query params (e.g.
merchant=123&view=boost) or break the URL. Encode it with encodeURIComponent.
Place ids are numeric today (so it encodes to itself), making this defensive;
the round-trip test is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aligned #1003

The icon-size lift scaled only the pin layers (unclustered-point/boosted-point),
but the comment-count, saved and label layers are separate symbols pinned to
fixed pixel offsets computed for icon-size 1, so a scaled-up pin left its badge
detached. The pulsing locator is the primary selection cue, so drop the lift
rather than re-scaling eight badge/label layers and their offsets.

Removes setSelectedPinLift + its reactive and the now-unused SELECTED_PIN_SCALE
/ PIN_LAYER_IDS / liftedPinId / ExpressionSpecification import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@escapedcat

Copy link
Copy Markdown
Contributor Author

@dadofsambonzuki please give it try, re-added the highlight

@escapedcat escapedcat merged commit 0513904 into main Jun 5, 2026
12 checks passed
@escapedcat escapedcat deleted the fix/view-on-map-merchant-deeplink branch June 5, 2026 14:03
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.

3 participants