Data-driven pass (adds rollup-plugin-visualizer behind ANALYZE=1):
- MapPage was statically imported by the router, dragging the map route AND the
IndexedDB offline-cache layer into the entry -> now lazy()
- NewsletterOverlay imported NEWSLETTER_ISSUES from the whole NewsletterPage
module -> types + data moved to lib/newsletterIssues.ts
- SasquatchGuide (assistant overlay) lazy-loaded in Layout
- Cashu stack is now a separate chunk (useCashu), off the critical path
Net: entry down ~61%; first paint no longer parses the map, the newsletter
page, the assistant or the wallet stack.
- Layout (app shell) imported useCashu, pulling the Cashu stack into the entry
chunk every visitor downloads. The three balance widgets now live in
components/wallet/CashuBalance.tsx and load via React.lazy.
- useNWC statically imported @getalby/sdk; it is now a type-only import plus a
dynamic import at call sites, so the SDK only loads when NWC is used.
Entry chunk: 1879 KB -> 1835 KB.
PageHeader now carries the app's neon glow (accent-tinted text-shadow), so the
unified header keeps the brand look rather than flattening it. Four more plain
headers migrate (title + accent + optional subtitle/actions).
- New shared PageHeader + PageContainer (width tokens: narrow/list/detail/wide)
to end the max-w drift between sections; piloted on ContactsPage
- Profile: the identity/contact sidebar now comes first on mobile (order-first /
lg:order-none) instead of being buried under every content section
showAwardDialog was never set true, so the dialog could never open; the inline
AwardBadgeInline form is the live flow. Removes ~142 lines plus its now-unused
Dialog imports, state and helper.
The calendar always initialised to the current month, so the Past tab (all
events in the past) opened on an empty month. It now anchors to the newest
past event, and re-anchors when the tab changes.
- Profile showed the live status indicator twice (avatar + name row) -> once
- Stash/Wallet informational text bumped from gray-600/700 to gray-400/500 for
readable contrast on the near-black background
- Map view now shows a 'No spots match / Clear filters' overlay when the active
filters hide everything (it previously looked blank/unloaded)
- 'List Gear' passes returnTo=/marketplace, so the back link and the post-publish
redirect return to Marketplace instead of dumping users on the map
The '$40k/BTC' era confirm-payment dialog was unreachable after the Pay button
was removed — deleted it, its handler and state (and the now-unused useCashu
import). In its place, listings show '≈ N sats' under the price using the live
rate, so buyers still see the sat amount.
- Bounty leaderboards no longer slice to top 50 before computing the current
user's rank (anyone outside the top 50 never saw their position), and each row
no longer prints the count twice
- Lab craft tabs: 4-up grid (11 tabs were laid out 5-across, leaving a lone
'Judges' orphan row); stale comment corrected
- Contacts search placeholder now says it filters by npub (it never matched the
displayed names)
- ShopDetailPage gallery reuses ImageLightbox: photos are now zoomable and the
whole set is reachable (it showed at most 5 and clicks did nothing)
- FAQPage gains static quick answers so the page is useful even if The
Sasquatch guide fails to open (it previously had zero intrinsic content)
- SpotPicker showed 'No spots found' during the initial load (also affected
CreateListing); now shows a loading state first
- Events: switching to the calendar view clears the date-window filter, which
only has chips in list view — it was silently hiding calendar events
- EmptyState: render <Link> as the button (a <button> inside <Link> was invalid
and double-focusable); AboutPage <a href> -> <Link> (no full reload)
- aria-label on icon-only controls: settings Back, conversation Refresh/Send,
crew chat Send
- Crew invite Accept/Decline raised to 44px touch targets
- Add Marketplace to the More menu (it had no primary-nav entry)
- CLINK: explain the copy/QR fallback when no CLINK-compatible wallet handles
the deep link, so 'Open in CLINK wallet' is not a silent no-op
- Sats prices used a hardcoded $1 = 2500 sats (i.e. $40k/BTC), so buyers were
quoted wrong amounts; now uses the live useUsdToSats rate, handles sats/BTC
directly, and shows no estimate for unsupported fiat
- Removed the 'Pay with Cashu' button, which only ever showed a 'coming soon'
toast — a dead affordance. Buyers use Contact Seller.
- geolocation: cap the overall deadline at 15s (was 25s) and shorten the native
plugin attempt to 8s, so a failed fix no longer hangs ~25-40s
- MapPage: only warm the location cache when permission is ALREADY granted, so
opening /map no longer fires the browser's 'Allow location?' prompt
- MapPage: add a desktop Near Me control (it was mobile-only)
- MapPage: on native, a denial is now retryable instead of permanently opening
the browser-only help sheet
Adds a shared ErrorState/PageError with a retry action. Detail pages (spot,
shop, collection, coach, crew, event) now show an honest failure + Try again
instead of '404 Not Found' — a relay hiccup previously implied the content was
deleted. The zap/bounty leaderboards surface errors instead of 'no content yet',
and the DM inbox gains a retry button.
- Stash: remove the handler-less 'Share via DM' button; action overlay stays
visible on touch (was hover-only, so edit/delete were unreachable on mobile)
- Crew invite accept no longer claims membership is confirmed (only the founder
ratifies the roster) — both the toast and the accepted state now say so
- Crew invite share message no longer promises a one-click join
- Map marker: don't bind the Leaflet popup when a host supplies onSpotClick
(MapPage's bottom sheet) — both used to appear at once
- SpotDetailPage: 'Claim This Shop' was a no-op button; wire to NIP-05 settings
(and stop the full-page <a href> reload)
- TourDetailPage: 'Contact Guide to Book' used /messages?to= which nothing read,
so booking silently opened an empty inbox -> /messages/:npub
- ContactsPage: row 'Message' went to the generic inbox -> the contact's thread
- SettingsPage: ?section=follows|language|stash rendered a blank dead end;
unknown sections now fall back to the menu
- DirectMessages/Conversation: logged-out states were dead ends -> add LoginArea
- Conversation: failed sends were silent -> error toast
Deploys replace hashed chunk filenames; a tab that was already open can load a
chunk that no longer exists (404) and hit the error boundary ('THIS PAGE
SLAMMED'). Listen for Vite's vite:preloadError (and dynamic-import failures) and
reload once, guarded against loops.
CreateEventPage gains an optional 'Funding Goal (sats)' field that publishes a
'goal' tag. EventDetailPage shows a FundingGoalCard with live raised/target
progress, backer count and a Fund button — funded peer-to-peer via NIP-57 zaps
(no escrow).
Derives First Zap / Regular / Generous / Patron / Top 10 from the batched
30-day supporter leaderboard and shows earned vs locked badges in the profile
sidebar. Client-side recognition only — publishes no events.
Shows the four most-zapped spots (last 30 days) on the home page, with their
sats totals, linking through to /leaderboards. Renders nothing when there is
no zap activity or no linkable spot, so the section never appears empty.
New /leaderboards route (linked from the More menu): aggregates kind-9735
receipts into top-zapped spots and top supporters, with week/month/all-time
tabs. Reuses the batched zap helpers. Also hardens the deploy smoke test to
boot lazy routes (/, /leaderboards, /bounties), since route chunks can fail
independently.
SortDropdown offered 'Most Zapped' but MapPage's sort switch had no zaps case,
so picking it silently fell back to newest-first. Adds a batched useZapTotals
hook (ONE receipt query for every location) and implements the sort.
ZapButton already fetched zap receipts for every visitor but returned null
unless you were logged in, so visitors saw no social proof. Now it renders a
read-only '⚡ <total>' whenever there are zaps (with a zap-count tooltip) and
only requires login to OPEN the zap dialog. No extra network cost — the query
already ran unconditionally.
Gallery images were inert (click did nothing) and only the first five were
reachable. Adds ImageLightbox: click any photo to view full size, arrow-key /
button navigation across the whole gallery, position counter, caption and
community-photo attribution. Extra images show a +N overlay.
The vendor-* split created circular imports; the production bundle died with
'ReferenceError: Cannot access X before initialization' (vendor-charts, then
vendor-bitcoin). Dev mode and vite build do not execute the bundle, so it only
surfaced once deployed. Rollup's default splitting is cycle-safe.
The lock from f25fc2b had package entries stripped of their dependencies/bin
metadata, leaving @nostrify/nostrify and @capacitor/cli (13/17 deps) with
missing transitive packages. Rebuilt the metadata from the installed manifests
and let npm fill only the gaps: 654 -> 902 packages, 3 existing versions changed
(debug, minipass, ws).
The spot page (and the other detail pages) used `grid lg:grid-cols-3` with
no explicit mobile column. Below the `lg` breakpoint the grid falls back to
a single implicit `auto` track, whose minimum is the min-content of its
items. A long mapper name ("Tip the Mapper" card) gave the sidebar a
~559px min-content, so the whole grid widened past the viewport and was
clipped by the global `html, body { overflow-x: hidden }` — content cut off
on the right with no way to scroll to it.
Fixes:
- Add `grid-cols-1` to detail-page grids so the mobile track is
`minmax(0, 1fr)` and can never exceed the container. Applied to Spot,
Shop, Event, NIP-19, Crew and Coach detail pages.
- Add `min-w-0` to their sidebar/main columns so long content can shrink
and truncate instead of forcing the track wider.
- Make the Spot Chat header wrap (`flex-wrap` + `gap-y`) so the trust
filter and channel badge no longer get clipped on narrow phones.
Adopt PosterChan's "encrypted events are the datastore" idea for two
per-browser bits of state, so they follow the user's npub across devices.
New shared engine (src/hooks/useSyncedAppData.ts):
- Stores one encrypted kind-30078 (NIP-78) event per d-tag, NIP-44
encrypt-to-self, modeled on the proven useStash pattern.
- Clobber-guard: re-fetch + re-decrypt the freshest remote copy and
MERGE the local patch in before publishing (never overwrites a field
another device just set).
- Debounced publisher (coalesces rapid writes, flushes on pagehide/
beforeunload) so frequent read-marker changes don't spam relays.
- No-op for anon / non-NIP-44 signers.
Read-state sync (src/hooks/useReadState.ts), d-tag skatespots:read-state:
- Unifies the four previously localStorage-only markers (notification
bell, DMs, crew chat, newsletter) into one synced blob.
- Merge = field-wise MAX (markers only move forward → commutative, safe).
- The four legacy hooks now re-export drop-in replacements with identical
signatures, so NO consumer component changed. localStorage stays as the
instant/offline/anon fast path; the relay copy hydrates and max-merges.
- Fixes the real bug where clearing a badge on one device left it lit on
another.
Preference sync (src/hooks/usePreferences.ts), d-tag skatespots:preferences:
- Mobile + desktop nav layouts now sync (language field wired for later).
- Merge = last-write-wins by updatedAt (a pref replaces, not accumulates).
- useNavPreferences / useDesktopNavPreferences adopt a synced layout when
present and mirror changes; public APIs unchanged, localStorage fallback
intact.
Docs: NIP.md gains a "Cross-device app data (kind 30078, encrypted)"
section documenting both d-tags, their content shapes, merge policies, and
the clobber-guard/debounce/anon safety notes.
Build + typecheck pass.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The bolt11 amount decoder is correct — the three failing tests used the
wrong invoice fixture. Per BOLT11, `lnbc2100n` = 2100 x 1e-9 BTC x 1e8
sats = 210 sats, not 21. A 21-sat invoice is `lnbc210n`. The passing
`lnbc1000n = 100 sats` case confirms the decoder's math.
Corrected the three cases in bolt11.test.ts and verifyInvoice.test.ts
to use `lnbc210n` (21 sats) so they exercise the intended 21-sat / 1-sat
slack scenarios against the correct implementation.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Regenerate lockfile so @tanstack/react-query (and the related
query-core / persist-client packages) resolve to 5.102.8, matching
package.json. The committed lockfile was stale at 5.75.1, which made
`npm ci` abort with "lock file's @tanstack/react-query@5.75.1 does not
satisfy 5.102.8" and left vitest running against a half-broken
node_modules.
- Use path.resolve(__dirname, './src/test/setup.ts') for vitest
setupFiles so the setup module is located relative to the config file
rather than the invocation cwd, fixing the "Cannot find module
.../src/test/setup.ts" resolution failure.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Replace single-assignment `let timer` with a `timers[]` array in
citrine.ts and relayProbe.ts so stricter prefer-const builds pass
(the newer ESLint pulled in by `npm audit fix` flagged the let as
never-reassigned).
- Drop two now-redundant `eslint-disable react-hooks/exhaustive-deps`
directives in NostrProvider and MapPage. Newer react-hooks no longer
reports those lines, so the directives were flagged as unused errors
under reportUnusedDisableDirectives; removing them is safe in both
old (warning only) and new (silent) ESLint.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two type errors (surfaced by `npm run test` / tsc --noEmit on a full
toolchain; the in-browser bundler does not run tsc, so they were latent):
- useCVMClient.ts: remove a malformed cast
`spots as typeof spots & { tags; created_at }[]` feeding
deduplicateByIdentity(). The intersection dropped pubkey/kind and broke
the reassignment to `spots`. deduplicateAddressable() already returns
the correct shape, so the cast was both wrong and unnecessary.
- queryPersister.ts: import PersistedClient / Persister from
@tanstack/query-persist-client-core (their canonical source) instead of
@tanstack/react-query-persist-client, whose `export *` re-export tsc
won't resolve for named type imports. Added the core package (5.102.8,
matching the pinned wrapper) as an explicit dependency.
Both are pre-existing issues on the branch, unrelated to the audit
remediation commits.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>