Commit graph

216 commits

Author SHA1 Message Date
shakespeare.diy
05463eabb4 newsletter: add issue #002 — map redesign, settings iOS overhaul, wallet, rider card, v1.0.54
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:29:06 +07:00
shakespeare.diy
ecf8e0967b fix: add missing SPOT_TYPE_LABELS import to MapPage (SpotBottomSheet crash)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:24:59 +07:00
shakespeare.diy
54e135d976 fix: add missing MapPin import to MapPage (SpotBottomSheet crash)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:23:20 +07:00
shakespeare.diy
f4cdafa145 card: full UX rebuild — hero preview, 3-tab nav, single-column layout, inline award form
- Logged-out state: proper hero with value prop copy instead of bare login form
- Hero card preview: full-width dark stage with theme-matched glow behind card
- Card slightly larger (360x227) with depth shadow
- Top tab bar: My Card / Badges (with count) / Award — replaces buried sections
- Single-column max-w-2xl layout — no more awkward 2-col grid on mobile
- Primary CTA: one big Share button, Print/Save/Copy as secondary 3-col row
- Card type as prominent 2-up tile choice, not a buried dropdown-style control
- Theme picker: 6-up square grid with emoji + label, ring on active
- Rides For + Sponsors side by side (saves vertical space)
- Stats as interactive tiles showing the actual numbers
- Skills as pill grid with live counter (X/6 selected)
- Badges tab: shows ReceivedBadgesPanel (was previously never rendered)
- Award tab: inline form, no dialog — cleaner flow, redirects to Badges on success
- AwardBadgeDialog kept for backward compat but Award tab uses AwardBadgeInline

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:20:26 +07:00
shakespeare.diy
2e32605948 map: consolidate right-side controls into single column (pills → divider → Quick Snap)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:10:24 +07:00
shakespeare.diy
c9bb5abfff sasquatch: left-anchor panel on mobile, leave right side free for Quick Snap + filter pills
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:08:03 +07:00
shakespeare.diy
be4e0bc986 map: remove duplicate sasquatch, reposition original bubble to bottom-right below Quick Snap
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:05:46 +07:00
shakespeare.diy
cc2e3d3165 map: replace Vibed with Shakespeare with Sasquatch trigger button below Quick Snap
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:03:19 +07:00
shakespeare.diy
889eb922eb map: move attribution to top-right below Near Me, add Vibed with Shakespeare below Quick Snap
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 20:01:28 +07:00
shakespeare.diy
f8b14f1056 map: switch tile layer to CartoDB Voyager
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:59:11 +07:00
shakespeare.diy
682e4d802b map: dark tiles, near me button, bottom sheet, floating filter pills
- Swap OSM tiles for CartoDB Dark Matter (near-black bg, neon markers pop)
- Add floating Near Me button (top-right, crosshair icon, flies to user location)
- Add SpotBottomSheet: tap a marker → drawer slides up from bottom with
  spot thumbnail, title, location, type badges, author, View Details + Directions CTAs
  (map stays fully visible behind the sheet)
- Add flyTo prop to SpotMap for programmatic map navigation
- Add floating layer filter pills (spots/shops/gear/tours) on map surface
  bottom-left, always visible, no toolbar required

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:57:06 +07:00
shakespeare.diy
1ce2837fef settings: replace 7-tab horizontal scroll with iOS-style vertical menu + sub-screens
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:32:38 +07:00
shakespeare.diy
f0eadf307b wallet: remove duplicate balance hero, single CashuWalletCard as source of truth
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:29:52 +07:00
shakespeare.diy
1876d14b0f wallet: mobile-first redesign - balance hero, compact status pills, collapsible educational content
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:27:14 +07:00
shakespeare.diy
df8bd39ce1 fix: push Leaflet attribution above mobile nav bar on /map
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 19:23:57 +07:00
shakespeare.diy
456f42a262 events: apply same UX pattern as /lab — sticky toolbar, filter drawer, location filter, active chips
- Replace scattered filter controls (search + Select dropdowns + ScrollArea chips + TrustFilter dropdown + SortDropdown) with the same unified toolbar + filter drawer pattern used on /lab
- Sticky toolbar: Events/Tours tab toggle with live counts, Upcoming/Past sub-tabs (events only) with counts, inline search + city location inputs, filter button with active count badge
- Responsive filter drawer: bottom sheet on mobile, right side panel on desktop (md:)
- Filter drawer: Sort pills, Trust Level inline pills (no nested dropdowns), Event Type chips (events) or Tour Category + Skill Level chips (tours) — all in one place
- Active filter chips row: selected event types as dismissible pills, sort/trust badges, "Clear all" — only visible when filters are active
- CreateFAB: replaces plain Link button with a dropdown for both "Create Event" and "Create Tour" from any view
- Unified EmptyState component with context-aware "Clear Filters" vs. "Create" CTA
- Dedicated location filter input split from general search (City field)
- Results count shown below toolbar, no empty-height gaps while loading
- Removed: Input, ScrollArea, ScrollBar, Select, Tabs, TabsList, TabsContent, TabsTrigger, TrustFilter, SortDropdown imports (all replaced inline)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 17:02:46 +07:00
shakespeare.diy
41ede8a122 lab: UX improvements — location filter, responsive drawer, inline trust pills, active filter chips
- Add dedicated City location input field in toolbar alongside search (splits "Name/bio" and "City" search into two focused inputs)
- Fix Filter drawer responsiveness: bottom sheet on mobile, right side panel on desktop (md:) 
- Replace TrustFilter compact dropdown in drawer with inline pill buttons (no nested dropdowns)
- Add active filter chips row in toolbar showing selected skills as dismissible pills, sort/trust badges, and "Clear all" button — only visible when filters are active
- Tab count badges now always show (including zero) to give clear feedback
- Fix results count row: remove empty min-h gap, now only renders when not loading
- Search now also matches against genUserName fallback for pubkey-derived names

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:57:49 +07:00
shakespeare.diy
0dc0d272e1 fix: filter panel overlap, Wallet & Stash rename, Sasquatch on all pages
Filter panel (MapPage):
- Sheet now h-[82vh] max-h-[680px] with flex-col layout
- Scrollable content area uses flex-1 min-h-0 (proper flex children)
- "Show Results" button is now flex-shrink-0 at bottom of column
- No more overlap between Show Results and Stash/Tour quick action buttons

Wallet & Stash rename:
- Settings tab renamed from "Wallet" to "Wallet & Stash"
- "Me" dropdown in mobile header shows "Wallet & Stash" for the wallet link

Sasquatch persistent button (all pages):
- Minimized bubble is now ALWAYS visible on every page (not just pages with a guide message)
- Tapping the bubble on any page opens Sasquatch with a generic helpful greeting
- Pages with specific context (/, /map, /lab, /onboarding) still show their custom message
- Pages without context (/wallet, /settings, /stash, /events, etc.) now show a generic "Ask me anything" greeting with common questions
- Removed the separate X close button (minimize was already the same) — bubble stays visible as entry point at all times
- onPointerUp now also sets isVisible=true so panel opens on pages without a message

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:45:11 +07:00
shakespeare.diy
340f6fd89f fix(mobile): redesign wallet page UI for mobile + global mobile polish
Wallet page (/wallet):
- Add BalanceHero banner at top showing Cashu balance + wallet status pills
- Cashu/Lightning tabs with colored active states (orange/yellow)
- Remove nested balance display in CashuWalletCard (now shown in hero)
- Compact balance row in CashuWalletCard
- Stacked full-width inputs on mobile (no side-by-side on small screens)
- h-11 touch targets on all inputs and buttons (44px min)
- inputMode="numeric" on number fields (prevents iOS zoom)
- Receive/Send tabs with green/red active colors for clear context
- History items now 52px min height with colored amount text
- WalletComparisonGuide stays collapsed by default
- Fix: add Nut icon import to CashuWalletCard (was crashing)
- All modals use w-[calc(100%-1rem)] for proper mobile sizing

Settings page (/settings):
- 7-tab bar now scrollable horizontally on mobile (no overflow/wrapping)
- -mx-3 bleed trick so scrollable area goes edge-to-edge
- Wallet tab buttons full-width on mobile
- Container max-w-3xl for better desktop centering

FAQ page (/faq):
- Compact hero on mobile (smaller icon, tighter padding)
- py-6 on mobile instead of py-12

CSS utilities:
- Add xs: breakpoint (375px) variants: flex, flex-row, px-4, -mx-4, gap-2, text-xs/sm, hidden

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:35:49 +07:00
shakespeare.diy
9da1796774 fix: cap map zoom out, prevent grey bands and tile duplication
- minZoom: 2 — stops user zooming past a single world view
- maxBounds: full lat range with infinite lng — locks vertical pan to the world
- maxBoundsViscosity: 1.0 — hard clamp, no elastic bounce into grey space
- TileLayer minZoom: 2 to match

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:21:58 +07:00
shakespeare.diy
e8151bee17 revert: remove 3D globe, restore flat map as default
- Removed SkateGlobe component from view toggle
- ViewMode back to 'map' | 'list', defaulting to 'map'
- Removed Globe import, selectedGlobeMarker state, and all globe render blocks
- Flat Leaflet map loads as default again

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:18:24 +07:00
shakespeare.diy
6331a937f2 feat: map as homepage, 3D globe, camera fix, Sasquatch upgrades, UI/UX polish
## 1. Map as Homepage
- `/` now renders MapPage (the globe/map) — removed old Index homepage
- `/home` redirects to Index (legacy), `/map` alias still works
- Navbar: removed HOME link, SPOTS (MapPin) is now first nav item
- isActive() treats both `/` and `/map` as active for SPOTS tab

## 2. 3D Globe View
- New SkateGlobe component using pure Three.js (no h3-js/CDN issues)
- Earth night texture with atmospheric neon red glow
- Spot pins (red) + shop pins (lime) on globe surface
- Click pin → zoom camera in + show info card
- Drag to rotate, scroll/pinch to zoom, auto-rotation when idle
- Globe toggle button added to map/list view switcher
- Selected marker ring + info card with View Spot/Shop links
- Lazy-loaded with Suspense skeleton

## 3. Settings → Stash Link
- Added My Stash card in Settings > Wallet tab
- Links to /stash with Package icon and description
- Smooth hover animation with ChevronRight

## 4. Camera Fix (Capacitor Native)
- CameraCapture now detects native Capacitor platform at runtime
- On native Android/iOS: uses @capacitor/camera (OS-level camera, no WebView permission popup)
- On browser: falls back to getUserMedia stream (existing behaviour)
- Added Upload from Gallery fallback when camera permission denied
- Better error handling for user cancellation

## 5. Sasquatch Upgrades
- FAQ page: replaced static accordion wall with Sasquatch-powered UI
  - Quick question chips that pre-load questions into Sasquatch
  - "Open The Sasquatch" CTA button
  - Quick facts grid, clean modern design
- About page: added Sasquatch CTA card at bottom
- SasquatchGuide: listens for sasquatch:open and sasquatch:ask custom events
  - External pages can now trigger and pre-fill the Sasquatch
  - Sasquatch now shows even without a page-level message prop (for external triggers)

## 6. UI/UX Audit & Polish
- Focus rings: on-brand red (WCAG accessible)
- Card hover micro-interactions: subtle translateY + shadow
- Button/link transition consistency (150ms ease)
- Reduced motion support (@media prefers-reduced-motion)
- Modal backdrop blur improvement
- Skeleton shimmer animation utility
- Globe CSS utilities (touch-action: none for canvas)
- Typography helpers (text-balance, text-pretty)
- Improved map-full-height responsive calculation

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-22 22:47:39 +07:00
shakespeare.diy
cf1a18e941 Fix NUser constructor call: use positional args not object
NUser takes (method, pubkey, signer) as separate args.
Was passing {pubkey, signer} as first arg so pubkey was always undefined.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 15:53:21 +07:00
shakespeare.diy
52bf618b51 Fix amber login type: use x-amber instead of amber
NLoginOther requires type x-${string} to be a valid NLoginType.
Using bare 'amber' meant pubkey was never properly stored/rehydrated
from localStorage — it was always undefined/empty.

Using NLogin('x-amber', pubkey, ...) correctly stores pubkey through
the NLoginBase interface and survives localStorage serialization.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 14:57:20 +07:00
shakespeare.diy
dd650ee57c Show stored pubkey in Settings when profile not loaded (debug)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 14:42:47 +07:00
shakespeare.diy
de5de7abec Fix getPublicKey: JSON parse JSObject result + restore AMBER_PACKAGE arg
- JSObject from Capacitor needs JSON.parse(JSON.stringify()) to be
  accessible as plain JS object with property access
- Restore AMBER_PACKAGE as argument (null was causing broken URI)
- Remove debug toast from LoginDialog

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 14:28:58 +07:00
shakespeare.diy
0b1bf05e74 Fix useLoggedInAccounts: safely extract pubkey from amber login type
NLoginType doesn't expose pubkey field directly — amber login is cast
as unknown as NLoginType so l.pubkey was undefined. Use getPubkey()
helper that casts to extract pubkey safely for all login types.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 14:06:01 +07:00
shakespeare.diy
fc1c8aca26 Add visible debug toast showing pubkey returned by Amber after login
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 13:50:28 +07:00
shakespeare.diy
1c577f7392 Add debug logging to getAmberPublicKeyViaPlugin to trace exact return value
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 13:43:26 +07:00
shakespeare.diy
a6c220c911 Increase useAuthor timeout and retries for Capacitor WebView
WebSocket connections take longer to establish on first load in
Capacitor. Increase timeout from 8s to 15s and retries from 3 to 5.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 13:34:18 +07:00
shakespeare.diy
0d9cfa61fb Fix getPublicKey: pass null packageName to force Amber UI path
The Content Resolver silent path returns a cached/wrong key.
Passing null skips it and always opens Amber UI so the user
explicitly selects their key and the correct pubkey is returned.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 13:28:26 +07:00
shakespeare.diy
da1d208579 Fix getPublicKey: Java returns hex not npub despite field name
NostrSigner.npubToHex() is called before putting result in "npub" field,
so the value is already a 64-char hex pubkey. Skip nip19.decode() and
use it directly. This was causing the "data must be at least 8 chars" error.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 12:50:37 +07:00
shakespeare.diy
9eda3d5c86 Fix capacitorAmberSigner: rewrite with correct positional API
The actual plugin source uses positional args, not object params:
- getPublicKey(packageName)
- signEvent(packageName, eventJson, id, npub)
- nip04Encrypt/Decrypt(packageName, text, id, pubKey, npub)
- nip44Encrypt/Decrypt(packageName, text, id, pubKey, npub)
- isExternalSignerInstalled(packageName)

Previous implementation was based on wrong README docs.
Now based on actual dist/esm/index.js source.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 12:34:10 +07:00
shakespeare.diy
0f758d7b1f Fix capacitorAmberSigner: correct named export and setPackageName API
- Use named export { NostrSignerPlugin } not mod.default
- setPackageName takes a plain string not { packageName: string }
- isExternalSignerInstalled takes packageName as argument
- These were the exact bugs causing "plugin not available" error

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 12:25:25 +07:00
shakespeare.diy
ef22eb2de7 Fix Amber "not installed" false positive - skip isExternalSignerInstalled() check
Android 11+ package visibility rules cause isExternalSignerInstalled() to
return false even when Amber is installed, unless <queries> is declared in
AndroidManifest.xml. Skip the pre-check and attempt login directly instead,
with better error messages for cancel vs not-found vs other errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 11:49:16 +07:00
shakespeare.diy
6230948383 Fix build: externalize capacitor plugin + remove duplicate lab i18n keys
- vite.config.ts: mark nostr-signer-capacitor-plugin as external so
  Rollup doesn't try to bundle it (it's resolved at runtime by Capacitor)
- en.ts / es.ts: remove duplicate lab key that was causing esbuild warnings

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 11:40:53 +07:00
shakespeare.diy
8256f05ca8 Wire up nostr-signer-capacitor-plugin for Amber login and signing
- Add CapacitorAmberSigner: a full NostrSigner implementation backed by
  the native nostr-signer-capacitor-plugin (login, signEvent, nip04,
  nip44 all go through Amber via native Android intents)
- LoginDialog: on Capacitor, call getAmberPublicKeyViaPlugin() directly
  instead of the intent:// URL redirect — returns a Promise, no
  callback URL or polling needed. Shows clear error if Amber not installed.
- useCurrentUser: on Capacitor amber logins use CapacitorAmberSigner
  so every publish/sign goes through the plugin, not window.location.href
- Web/PWA path unchanged — still uses URL redirect + AmberSigner

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-09 11:38:23 +07:00
shakespeare.diy
7d071787d0 Fix Amber login for ZapStore/Capacitor app
- LoginDialog now uses openAmberForLogin() which sends the proper
  intent:// URL with a callbackUrl instead of the old nostrsigner:
  scheme that only copies to clipboard (broken on most setups)
- Add polling loop (500ms, 3min timeout) for amber_pending_pubkey
  written by AmberCallbackPage — works even when Amber opens the
  callback in a new browser tab (GrapheneOS/Android behaviour)
- visibilitychange listener now waits for app to regain focus before
  attempting clipboard read, fixing race condition
- DeepLinkHandler pre-captures ?event= param into sessionStorage
  when Amber fires appUrlOpen deep link in Capacitor, so
  AmberCallbackPage can always find the result
- Add 'paste' to amberStep union type (was missing, caused TS error)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 22:29:26 +07:00
shakespeare.diy
f73cb0ec8c Add SNT4EVER as partner on Supporters page
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 16:44:51 +07:00
shakespeare.diy
6ed5ac23cb Invert SNT4EVER logo for contrast on dark background
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 16:37:38 +07:00
shakespeare.diy
2904df4eab Update SNT4EVER logo to new version
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 16:35:51 +07:00
shakespeare.diy
81dd69679c Add SNT4EVER as friend on Supporters page
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 16:12:28 +07:00
shakespeare.diy
132b6cae7c fix: Amber login using confirmed correct URL from Amber source code
Read Amber's actual IntentUtils.kt to confirm exact URL parsing:
- Strips "nostrsigner:" prefix
- URLDecodes the remainder
- Parses ?key=value&key=value query params
- "type" param controls operation
- No callbackUrl → copies result to clipboard

Correct URL: nostrsigner:?type=get_public_key&compressionType=none&returnType=signature

Flow:
1. Tap "Open Amber" → window.location.href = nostrsigner: URL
2. Amber opens, user approves, copies hex pubkey to clipboard
3. App regains focus → tries navigator.clipboard.readText() auto
4. If clipboard denied → shows paste input (accepts hex or npub1)
5. Login complete

Removed broken Capacitor plugin dependency (couldn't install git dep).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 12:22:58 +07:00
shakespeare.diy
9e6d6f1c39 fix: switch to Coracle's nostr-signer-capacitor-plugin fork + correct API
The chebizarro npm version has a different API to the Coracle fork that
Welshman/Coracle actually use in production. Now using:

  github:coracle-social/nostr-signer-capacitor-plugin#main

Correct API (from Welshman nip55.js source):
  NostrSignerPlugin.setPackageName(packageName: string)  // string not object
  NostrSignerPlugin.getPublicKey(packageName?: string)   // packageName as arg
  NostrSignerPlugin.isExternalSignerInstalled(packageName?)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 12:11:18 +07:00
shakespeare.diy
f010a61608 fix: use nostr-signer-capacitor-plugin for Amber NIP-55 login
All previous URL-scheme approaches (nostrsigner:, intent:) were fundamentally
wrong for a Capacitor app. JavaScript in a WebView cannot reliably dispatch
Android intents via URL navigation.

The correct solution — used by Coracle and other Capacitor-based Nostr apps —
is the nostr-signer-capacitor-plugin which communicates with Amber via native
Android intents through the Capacitor bridge:

  NostrSignerPlugin.setPackageName({ packageName: 'com.greenart7c3.nostrsigner' })
  NostrSignerPlugin.isExternalSignerInstalled()
  NostrSignerPlugin.getPublicKey() → { npub }

This is synchronous from the JS side, handles the entire Android intent
roundtrip natively, and returns the pubkey directly. No URL encoding,
no callback URLs, no redirects, no malformed request errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 11:59:04 +07:00
shakespeare.diy
92f317f5e5 fix: Amber login for GrapheneOS — clipboard + paste flow
GrapheneOS Vanadium browser blocks intent: URLs from web pages as a
security hardening measure. This is why every URL format we tried
caused "malformed request" — Vanadium was blocking the intent before
Amber ever saw it.

New flow that works on ALL Android browsers including GrapheneOS:
1. Tap "Open Amber" → navigates to nostrsigner: URL with no callbackUrl
   (Amber copies pubkey to clipboard when no callbackUrl is given)
2. User approves in Amber, comes back to app
3. App tries navigator.clipboard.readText() automatically on visibilitychange
4. If clipboard permission denied → shows paste input for hex pubkey
5. User pastes pubkey → logged in

No intent: scheme, no callback URL, no redirect loop. Simple and bulletproof.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 11:43:59 +07:00
shakespeare.diy
712b871e1a fix: Amber intent: format + # in callbackUrl breaks intent parsing
Three compounding bugs fixed:

1. WRONG FORMAT: Was using nostrsigner: directly, then intent: with encoded
   values. Amber README shows intent: with raw unencoded S.key=value params.

2. # IN CALLBACKURL: Our callbackUrl was https://.../#/amber-callback which
   contains a # — but intent: uses # as its own delimiter (#Intent;...;end).
   Having a # inside the callbackUrl value breaks the entire intent string.
   Fixed: callbackUrl is now https://.../amber-callback?event= (no hash).

3. HASHROUTER MISMATCH: Amber opens /amber-callback (no hash) but HashRouter
   only handles /#/route paths. Fixed in main.tsx: pre-React IIFE detects
   /amber-callback path, saves ?event= to sessionStorage, then redirects to
   /#/amber-callback so React Router matches AmberCallbackPage correctly.

Final correct intent URL format:
  intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=https://skate-spots.org/amber-callback?event=;end

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 11:36:03 +07:00
shakespeare.diy
6f303d1956 fix: revert to nostrsigner: format, fix double-encoding of callbackUrl
Root cause of "malformed request":
- buildIntentUrl() was calling encodeURIComponent() on ALL values including
  callbackUrl, which was ALREADY a full URL with special chars
- This double-encoded the callbackUrl, arriving at Amber as garbage
- The intent: wrapper format was also unnecessary

Fix: back to clean nostrsigner: format per NIP-55 spec
- Only callbackUrl gets encodeURIComponent (once)
- All other params are plain query string values
- Matches exactly what NIP-55 spec shows in its example code

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-08 11:26:42 +07:00
shakespeare.diy
c30546f700 fix: use Android intent: URL format for Amber (not nostrsigner: directly)
The nostrsigner: scheme used directly caused "malformed request" in Amber
when opened from a browser context. The correct format per Amber's README
for web clients is the Android intent: URL format:

  intent:<payload>#Intent;scheme=nostrsigner;S.type=...;S.callbackUrl=...;end

Updated all Amber operations (login, sign, nip04/nip44 encrypt/decrypt)
to use buildIntentUrl() helper instead of raw nostrsigner: URLs.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-07 19:35:00 +07:00
shakespeare.diy
db157efda4 fix: use window.location.href for Amber NIP-55 intent (not window.open)
window.open(..., '_blank') opens a new browser tab instead of firing
the Android intent — causing Amber's "malformed request" error.

Per NIP-55 spec the correct method is window.location.href = url,
which triggers the nostrsigner: intent that Amber is registered to handle.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 14:17:51 +07:00
shakespeare.diy
59eda93641 fix: Amber login callback URL for HashRouter
BUG #1 (critical): buildCallbackUrl() was returning
  https://skate-spots.org/amber-callback?event=
but the app uses HashRouter, so React Router never matched /amber-callback.
Amber would open the callback URL in a new tab, land on the homepage, and
AmberCallbackPage would never render — login was silently broken.

Fixed to:
  https://skate-spots.org/#/amber-callback?event=

BUG #2: AmberCallbackHandler (global, in App.tsx) was competing with
AmberCallbackPage on the /amber-callback route, causing a race condition
where the global handler could consume the pubkey before the page component.
Fixed by detecting the hash path and early-returning when on /amber-callback.

BONUS: openAmberForLogin() now persists amber_return_path from the current
hash path so AmberCallbackPage can navigate back to where the user was.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 13:58:41 +07:00
shakespeare.diy
4f7852fec7 Fix Amber login: replace nostrconnect/relay flow with proper NIP-55 intent flow
The entire nostrconnect relay approach was wrong for a native Android/Capacitor app.
Amber on Android uses NIP-55 intents (nostrsigner: URI scheme), not NIP-46 relays.

New flow (same as Amethyst):
1. openAmberForLogin() fires nostrsigner:?type=get_public_key&callbackUrl=...
2. Amber opens, user taps approve
3. Amber calls back to org.skatespots.app://amber-callback?event=<pubkey>
4. AmberCallbackPage writes pubkey to localStorage('amber_pending_pubkey')
5. LoginDialog polls localStorage every 500ms and calls login.amber(pubkey)
6. Done — no relays, no waiting, no spinning

Also added login.amber(pubkey) method to useLoginActions.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 13:24:46 +07:00
shakespeare.diy
e3dd66d078 Fix Amber login: store as 'amber' type (NIP-55) not 'bunker' (NIP-46)
The nostrconnect flow was incorrectly creating a bunker login and trying
to do NIP-46 key exchange. Amber signs as the user's own key directly,
so the pubkey from the connect response IS the user pubkey. Now stores
a proper amber login which uses AmberSigner via useCurrentUser.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 12:38:56 +07:00
shakespeare.diy
03ea569a2a Amber login: poll relay every 3s after returning from Amber, add elapsed timer + Check Now button
- Replace one-shot visibility handler with pollRelay() called immediately on foreground + every 3s
- Add elapsed time counter (M:SS) so user can see the app is still trying
- Add 'Check Now' button for manual relay poll without generating a new URI
- Poll interval auto-clears on success, timeout, or cancel

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 12:34:20 +07:00
shakespeare.diy
dfb60674c5 Fix Amber timeout: increase to 3 minutes so user has time to approve in Amber and return
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 12:31:48 +07:00
shakespeare.diy
bd149fdb2c Fix Amber nostrconnect flow: 20s timeout, error state, stale closure fix, better relays, debug logs
- Replace wss://relay.primal.net with wss://nos.lol (more reliable for NIP-46)
- Shorten timeout from 10 minutes to 20 seconds
- Fix stale closure bug: track amberStep in amberStepRef so timeout can correctly reset UI
- Add 'error' step with red UI and 'Try Again' button instead of endless spinner
- Extend since filter from 5s to 10s in the past
- Add console logs for URI, relays, incoming events, decryption result, secret matching

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-03 11:48:03 +07:00
shakespeare.diy
24d71679af Layout: remove Sasquatch AI (/pyramid) from mobile nav and clean up Bot icon import
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-28 13:05:18 +07:00
shakespeare.diy
2c3697ba93 SupportersPage: use red transparent No Teeth logo
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 22:08:25 +07:00
shakespeare.diy
e03150c19f SupportersPage: remove Geyser from Friends, update No Teeth entry with white logo, Instagram link, and correct tagline
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 22:06:27 +07:00
shakespeare.diy
10b9d646fc SupportersPage: add No Teeth Skateboarding to Friends section with red logo
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 22:04:11 +07:00
shakespeare.diy
4aaa7007bf AboutPage: remove 'Learn About Nostr' card and nostr.com link
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 21:30:26 +07:00
shakespeare.diy
4ffa39ee95 Various UI cleanups: remove Nostr footer link, hide Map&Earn, update NostrHub links
- Index.tsx: remove Nostr.com button from homepage footer (next to FAQ/About)
- SupportersPage: change 'View on GitHub' → 'View on NostrHub' with correct naddr URL
- Layout: remove 'Map & Earn' (/bounties) from desktop More dropdown menu
- Layout: remove 'Map & Earn' (/bounties) from mobile slide-out nav
- Layout: clean up unused Gift icon import and stale isActive check

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 21:27:10 +07:00
shakespeare.diy
8633b38125 fix: friends CTA - only if you have the vibes
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 19:15:00 +07:00
shakespeare.diy
5c49946e5b fix: Geyser powered by emoji ⚡ -> 🧦
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 19:11:02 +07:00
shakespeare.diy
affe663291 fix: friends subtitle -> People we vibe with
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 18:26:52 +07:00
shakespeare.diy
80be48275e fix: update No Teeth tagline to Handmade Long Lasting Skateboards
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 18:02:26 +07:00
shakespeare.diy
089c6c234c feat: add No Teeth Skateboarding as first Friend, clear Partners
- Partners: removed all entries, now shows 4 placeholder slots
- Friends: added No Teeth Skateboarding with red transparent logo
  linking to instagram.com/noteethskateboarding
- Friends grid: always shows real entries + fills remaining slots to 5
  with "Coming soon" placeholders

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 17:57:41 +07:00
shakespeare.diy
baf4c5e10d feat: redesign /supporters with 4 sections + footer label update
- Supporters: existing Geyser leaderboard (unchanged)
- Collaborators: new section, empty placeholder with GitHub CTA
- Partners: Geyser, Angor, Zapstore, ContextVM, MKStack
- Friends: placeholder grid (5 slots) + logo spec instructions
- Footer: "Supporters" → "Collaborators & Friends"
- Static COLLABORATORS/PARTNERS/FRIENDS arrays at top of file for easy editing

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-27 17:51:11 +07:00
shakespeare.diy
5dffa5cac3 feat: UX improvements - credits, draggable sasquatch, mobile fixes
1. AboutPage: add Credits section (Zapstore, ContextVM, Angor, Geyser, MKStack,
   Shakespeare, Nostr), add Zapstore download button, fix powered-by text overflow
   on mobile (wrap-friendly flex layout)

2. SasquatchGuide: make minimized bubble draggable on all devices
   - pointer events for mouse, touch, and stylus
   - saves position to localStorage, persists across sessions
   - tap still opens, drag moves (5px threshold to distinguish)

3. Layout (burger menu): add overflow-y-auto + pb-8 to SheetContent so
   mobile menu is scrollable when content exceeds screen height

4. CashuWalletCard (wallet): fix input+button rows overflowing on mobile
   - flex-col on small screens, flex-row on xs+ (stacked on phones)
   - whitespace-nowrap on buttons to prevent text wrapping

5. LabPage: compact action buttons on mobile (h-8, text-[10px], px-2)
   so all 4 buttons fit in 2x2 grid without overflow/truncation

6. LoginDialog (Amber): improve visibility-change handler - use fresh
   SimplePool subscription on each foreground return instead of querySync
   (which doesn't exist in nostr-tools v2.13)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-23 09:33:46 +07:00
shakespeare.diy
84d80ac3a7 fix: handle Capacitor WebSocket suspension in NIP-46 Amber login
- Add visibilitychange listener: when app returns to foreground after Amber,
  does a one-shot relay query to catch Amber's connect response
- Store pending nostrconnect session in localStorage so it survives app suspend
- Keep live relay subscription as fallback for web/PWA
- Add nsec login tab as reliable fallback option
- Add proper step states: idle -> connecting -> waiting -> success

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-19 00:38:38 +07:00
shakespeare.diy
789a3d7a60 fix: use nostr-tools SimplePool+nip04 for NIP-46 relay subscription
Replace NRelay1 async iterator (unreliable) with nostr-tools SimplePool.subscribeMany()
using onevent callback — same pattern as Ditto/Coracle/Nostrudel.

- Switch to relay.nsec.app, relay.damus.io, relay.primal.net (support ephemeral events)
- Use nip04.decrypt() from nostr-tools for Amber's connect response decryption
- Proper cleanup of pool and subscription on abort/close
- Fall back to signerPubkey as userPubkey if NConnectSigner.getPublicKey() fails

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-19 00:28:46 +07:00
shakespeare.diy
61795db997 fix: implement NIP-46 nostrconnect:// Amber login properly
- Fix useLoginActions.ts: remove generateNostrConnectParams, generateNostrConnectURI,
  NostrConnectParams, NostrConnectStatus which don't exist in @nostrify/react@0.2.17/login
- Rewrite LoginDialog.tsx Amber tab with proper NIP-46 client-initiated flow:
  - Generates ephemeral client keypair + random secret
  - Builds nostrconnect:// URI (no nostrsigner: scheme, no intents)
  - Opens Amber via window.open(_blank) - standard URI scheme Amber handles natively
  - Listens on relay pool for Amber's kind 24133 connect response
  - Decrypts with NIP-04, verifies secret to prevent spoofing
  - Uses NConnectSigner to call get_public_key
  - Creates NLogin bunker object and stores it persistently
- Bunker tab: direct NLogin.fromBunker() paste flow
- Extension tab: NIP-07 browser extension
- No clipboard hacks, no nostrsigner: scheme, no Android intent issues

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-19 00:02:21 +07:00
shakespeare.diy
916f067238 fix: rewrite login to use NIP-46 nostrconnect flow (Ditto pattern)
Completely replaces the broken NIP-55 intent approach with NIP-46:

- Amber tab: generates nostrconnect:// URI, opens Amber, waits for
  Amber to connect back via relay using NLogin.fromNostrConnect()
  (same pattern as Ditto/soapbox-pub)
- Bunker tab: paste bunker:// URI from Amber or nsec.app
- Extension tab: NIP-07 browser extension (desktop)
- useLoginActions: updated to match Ditto's implementation with
  nostrconnect(), generateNostrConnectParams, generateNostrConnectURI
- Clean mobile-first UI — no clipboard hacks, no intent tricks

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 23:49:55 +07:00
shakespeare.diy
e8bc072787 fix: replace NIP-55 Amber login with NIP-46 bunker flow
NIP-55 (nostrsigner: intents) is fundamentally broken from Capacitor
WebView — no combination of window.open, window.location.href, or
anchor clicks works reliably across all Android WebView versions.

Switch to NIP-46 (bunker://) which works everywhere via WebSockets:
- User opens Amber → Connect apps → Add app → copies bunker:// URI
- Pastes it into the login dialog
- NLogin.fromBunker() completes the handshake over a relay
- Login works in browser, PWA, and Capacitor APK identically

This is exactly how Ditto (soapbox-pub) implements Amber login.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 23:33:42 +07:00
shakespeare.diy
00d9b69f81 fix: use window.open(_blank) to trigger Amber's web/browser intent path
Amber has two code paths:
- Native path (no Browser.EXTRA_APPLICATION_ID): reads type from intent
  EXTRAS — impossible to set from a WebView URL navigation
- Web/browser path (Browser.EXTRA_APPLICATION_ID set): reads type from
  URL query params — triggered when WebView opens URL as browser intent

window.open(url, '_blank') causes Android WebView to fire a browser
ACTION_VIEW intent which sets Browser.EXTRA_APPLICATION_ID, routing
Amber to parse our nostrsigner: URL query params correctly.

Previous attempts:
- window.location.href: WebView intercepts as asset URL
- window.open(_system): strips query params  
- anchor click: goes through native path (no extras = INVALID type)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 23:18:02 +07:00
shakespeare.diy
f534c413fd fix: use anchor click to open nostrsigner: URLs in Capacitor WebView
window.location.href causes WebView to try loading nostrsigner: as an
asset URL. window.open(_system) strips query params. The only reliable
method in Capacitor WebView is creating a temporary <a> element and
clicking it — this correctly triggers Android's intent system with the
full URL intact.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 23:10:13 +07:00
shakespeare.diy
fdebfc0d37 fix: use window.location.href for nostrsigner: URLs instead of window.open(_system)
window.open(_system) was stripping query parameters from the nostrsigner:
URL, causing Amber to receive a request with no type parameter and
rejecting it as malformed.

window.location.href works correctly in Capacitor WebView — Android
intercepts nostrsigner: scheme via intent-filter with full URL intact.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 23:02:33 +07:00
shakespeare.diy
55856e2935 fix: use clipboard mode for Amber login in Capacitor APK
Capacitor native apps fail with "malformed nostrsigner request" when
using callbackUrl because Amber treats custom URI schemes as invalid.

Fix: detect window.Capacitor.isNativePlatform() and use the no-callback
clipboard flow (openAmberForLoginNoCallback) instead of the callbackUrl
flow. Amber copies the pubkey to clipboard, app reads it on
visibilitychange when user returns.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 22:56:28 +07:00
shakespeare.diy
3f654f4a35 fix: single bundle APK build, remove debug banner, clean build scripts
- Remove debug red banner from index.html and main.tsx
- Add android:build:apk script that uses CAPACITOR_BUILD=1 for single
  bundle output + strips Shakespeare build artifacts from dist/index.html
- Update all android:* scripts to use android:build:apk

Root cause of white screen: 100+ JS chunk files overwhelm Android
WebView local file serving. Single bundle with inlineDynamicImports
fixes this.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 22:51:49 +07:00
shakespeare.diy
379a5f7b34 debug: add visible HTML marker to diagnose white screen in APK
Red banner renders from pure HTML before any JS loads.
If visible → HTML loads but JS fails.
If not visible → WebView can't even load index.html.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 22:32:18 +07:00
shakespeare.diy
be70ff490f fix: remove @capacitor/app dynamic import from DeepLinkHandler
Use window.Capacitor.Plugins.App directly instead of dynamic import()
so the build doesn't fail when @capacitor/app isn't in node_modules

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 22:01:40 +07:00
shakespeare.diy
c3ef0247fc fix: switch to HashRouter for Capacitor Android compatibility
The primary cause of the white screen in the APK was BrowserRouter.
Capacitor serves files via https://localhost/ and the HTML5 History API
routing (BrowserRouter) fails to resolve routes in this context,
resulting in a blank white page.

Changes:
- Switch BrowserRouter → HashRouter in AppRouter.tsx
- Add DeepLinkHandler component (handles appUrlOpen for Amber callbacks
  and other deep links in native context)
- Fix Amber callback URL builder to use org.skatespots.app:// custom
  scheme when running in Capacitor (so Android fires an intent back into
  the app instead of navigating a browser)
- Fix getSearchParams() in amber.ts to extract query params from hash
  fragment (HashRouter URLs look like /#/amber-callback?event=XXX)
- Fix captureAmberCallback in main.tsx to check hash fragment too
- Remove debug error overlay (no longer needed)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 21:56:23 +07:00
shakespeare.diy
8c25110f44 debug: add visible error overlay in APK to diagnose white screen crash
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 21:41:32 +07:00
shakespeare.diy
dc66ac993f fix: use global Capacitor object to hide splash screen (no package import)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 21:30:29 +07:00
shakespeare.diy
4b61b14d36 fix: splash screen + ErrorBoundary dark bg for Capacitor APK white screen
- Add SplashScreen.hide() call in main.tsx so splash dismisses when JS loads
- Set launchShowDuration: 0 + launchAutoHide: true in capacitor.config.ts
- Fix ErrorBoundary to use bg-black text-white instead of bg-background
  (bg-background = white before dark class is applied = white screen on error)
- These three combined should eliminate the white screen in the APK

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 21:27:34 +07:00
shakespeare.diy
a677e8687d fix: Amber Capacitor APK + mobile UI improvements
Amber login (critical):
- Replace window.location.href with window.open(_system) in Capacitor WebViews
- Capacitor's WebView throws "malformed nostrsigner request" on href navigation
- New openAmberUrl() helper detects Capacitor via window.Capacitor.isNativePlatform()
- Add openAmberForLoginNoCallback() for PWA/Capacitor clipboard flow
- Fix all 6 Amber URL opens in amber.ts + LoginDialog.tsx

Mobile nav:
- Newsletter and Sasquatch AI (/pyramid) added to hamburger menu
- Sasquatch floating guide now shows on mobile (removed hidden md:block)

Lab page mobile:
- Action buttons (Offer Coaching/Filming/Editing/Get Card) now 2-col grid on mobile
- Service type filter (Coaches/Filmers/Editors) now 3-col grid, full width
- Reduced top padding on mobile (py-6 → py-12 on md+)
- Text truncates cleanly, icons don't overflow

Wallet page mobile:
- Reduced padding (py-6 md:py-8)
- Tab triggers use h-auto + py-2.5 to avoid overflow
- Smaller header text on mobile

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 20:48:11 +07:00
shakespeare.diy
576df73a6b fix: restore original SkateSpots logo (icon-1024.png) everywhere
Replace all icon.svg references with icon-1024.png across:
- index.html (favicon, apple-touch-icon, og:image, twitter:image)
- src/components/Layout.tsx (desktop header + mobile header)
- src/pages/Index.tsx (footer)
- public/manifest.webmanifest (PWA icon entry)

Also fix og:url and og:image/twitter:image to use correct domain
skate-spots.org instead of skate-spots.com

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 20:02:27 +07:00
shakespeare.diy
72cb690e64 fix: increase relay query timeouts and fix AppConfigSchema for cold mobile loads
- Raise QUERY_TIMEOUT in useSkateSpots from 8s to 15s
- Raise AbortSignal.timeout in useLabProfiles and useSkateEvents from 10s to 15s
- Add useSkateRelay to AppConfigSchema so it's preserved when reading from localStorage

On a fresh phone (no warm WebSocket, no TanStack Query cache), the app
needs to: load JS bundle, boot React providers, open a WebSocket to
goskatespots.nostr1.com, and receive EOSE — all before the timeout.
On slow mobile connections this easily exceeded the old 8-10s limits,
causing /home to show empty sections while /map worked (the connection
was already warm by then). 15s gives cold connections enough headroom.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 15:28:26 +07:00
shakespeare.diy
9ed2c12de1 fix: remove sponsor mention + add Nostr preview modal before publishing
- "Sponsors see the organic number" -> "Users can now see both numbers"
- PublishToNostrButton now opens a preview modal first (NostrPreviewModal):
  - Mock Nostr post card: avatar, display name, timestamp, post body
  - nostrText rendered with hashtags in blue and URL underlined
  - Fake engagement bar (replies/reposts/zaps at 0) for visual context
  - Character count shown below the card
  - Two actions: "Edit first" (closes modal) / "Publish" (confirms)
  - Full-width on mobile, correct safe-area handling via Dialog component
  - isPending state flows through so button shows "Publishing..." while signing

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 15:12:37 +07:00
shakespeare.diy
e4a9b454a4 fix: rename issue -> drop, fix hashtags, add 6 tags
- "Issue" replaced with "Drop" everywhere in UI and copy:
  Drop #001, Weekly Drops, Write Drop #002, next drop, etc.
- nostrText: hashtags consolidated to one line (some clients break
  each hashtag onto its own line when separated by newlines)
- nostrText: removed "Full issue:" prefix — bare URL unfurls cleaner
- 6 hashtags added: #skateboarding #nostr #skatespots #spotr
  #newsletter #skatespotsnews
- Publish button tags updated to include all 6 as t-tags
- Template comment updated to match new format

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 15:09:32 +07:00
shakespeare.diy
b5edb0ab52 fix: nostrText format — short teaser, URL as the star
Kind 1 notes render differently on every client (Damus, Primal, Amethyst,
Snort, etc). The fix: keep the kind 1 as a 5-line teaser that works as
plain text on any client, with the full formatted issue at the URL.

Issue #001 nostrText trimmed from 14 lines to 4.
Added template comment with rules for all future issues:
- Max 5 content lines before the URL
- No bullet lists
- URL always on its own line
- 3 hashtags max

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 15:00:26 +07:00
shakespeare.diy
4f7b741d77 fix: newsletter mobile layout — full phone support
- Page container: py-8 on mobile + pb-24 so content clears fixed bottom nav
- Masthead headline: single line (no br), text-4xl mobile / text-6xl sm+
- Issue header: vertical stack on all screens — date/badge row, large number,
  vibe text, then publish button full-width on mobile / auto on sm+
- Stats grid: always 3 cols (clean 2-row layout on phone), larger text-2xl
  numbers, text-[10px] labels readable on all Androids, shorter relay label
- Floating pill: max-w-[calc(100vw-1.5rem)] prevents overflow on narrow screens,
  right-3 tighter margin, whitespace-nowrap on text, touch-manipulation + 
  active:scale-95 for native tap feel, correct bottom offset for iOS safe area
- Monday admin banner: unchanged, already stacks correctly

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 14:56:30 +07:00
shakespeare.diy
248f5b5ff2 feat: newsletter v2 — structured issues, live stats, Nostr publish button
Structured issue schema (NewsletterIssue):
- updates[]: typed update items with icon/title/tag/body
- spotOfTheWeek?: optional typed spot block
- communityNote: free-text community section
- nostrText: plain-text ready to post as kind 1

Live community stats block (LiveStatsBlock):
- Pulls from useGrowthStats on every page load
- Shows spots, shops, events, mappers, coaches, tours
- Trusted/active account counts shown below spots + mappers
- "Live · pulled from goskatespots.nostr1.com" attribution

Publish to Nostr button (PublishToNostrButton):
- Only visible to 2 admin pubkeys — nobody else sees it
- Publishes kind 1 with nostrText + tags (skateboarding, skatespots, nostr)
- Includes relay URL tag pointing to /newsletter
- One-shot: button becomes "Published to Nostr" checkmark after success
- Full error handling with toast

Issue template now enforces consistent sections every week:
1. Community by the Numbers (live stats)
2. What's New (typed update cards)
3. Spot of the Week (optional — shows placeholder when omitted)
4. Community note
5. Support / Geyser

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 14:45:59 +07:00
shakespeare.diy
c146d94b9f feat: weekly newsletter system with Issue #001
- NewsletterPage (/newsletter) — zine aesthetic, dark bg, neon accents
  Issue #001 covers all 8 security measures shipped this week:
  geohash validation, URL sanitization, content limits, WoT defaults,
  trusted stats, OfficialBadge, marker clustering, DM phishing warning

- NewsletterOverlay — two overlays rendered globally via Layout:
  1. Floating "Latest Drop" pill — shows for 7 days after each issue,
     dismissible per-issue via localStorage, hides on /newsletter page
  2. Monday admin reminder — only visible to 2 authorized pubkeys
     (founder + second admin), shows every Monday, one-click opens
     Shakespeare AI with prompt pre-filled to write the next issue,
     dismisses for the rest of that Monday via localStorage

- Footer — Newsletter link added to desktop footer alongside License/Supporters

- AppRouter — /newsletter route added (lazy loaded)

To add Issue #002: prepend a new object to NEWSLETTER_ISSUES array
in NewsletterPage.tsx. The pill and Monday reminder auto-update.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 14:39:54 +07:00
shakespeare.diy
f19851da92 security: implement all 8 protection measures
## 1. sanitizeUrl() (already existed, verified applied)
- Applied to ShopDetailPage, CoachDetailPage with urlDomain() tooltip hints
- Blocks javascript:, data:, vbscript: and all non-http(s) schemes
- Returns null for invalid URLs — link simply doesn't render

## 2. Geohash validation in parsers
- Added isValidGeohash() in types.ts using ngeohash.decode()
- Min 4 chars (country-level precision rejected), max 12 chars
- Rejects lat/lng outside Earth bounds
- Rejects 0,0 "null island" — classic bot artifact
- VPN/Tor unaffected: geohash comes from map pin, not user IP

## 3. Content length limits in all parsers
- MAX_TITLE_LEN=100, MAX_DESCRIPTION_LEN=3000, MAX_LOCATION_LEN=200
- MIN_TITLE_LEN=2, MIN_GEOHASH_LEN=4
- Applied to parseSkateSpot, parseSkateShop, parseSkateCoach,
  parseSkateFilmer, parseSkateEditor, parseSkateEvent

## 4. Smart WoT default (already in codebase, verified correct)
- Logged out / <5 follows → 'all' (don't punish new/anon users)
- Logged in with ≥5 follows → 'trusted' (sort trusted first)
- Soft mode: trusted content floats to top, nothing hidden
- Active in MapPage and EventsPage

## 5. Trusted count stats in useGrowthStats
- Added trustedSpots and trustedMappers to GrowthStats type
- Heuristic: pubkeys with ≥2 published items = organic community member
- CommunityStatsPage now shows "🛡 X from active accounts" below spot count
- Purely additive — both numbers always visible

## 6. OfficialBadge component (already existed, wired up)
- Added to SpotCard, EventCard, ShopDetailPage, CoachDetailPage
- Only renders for FOUNDER_PUBKEY (20e7c953...)
- Zero-friction: invisible for all other pubkeys
- Impersonation attempts immediately visible

## 7. Leaflet marker clustering
- Installed leaflet.markercluster v1.5.3 + @types
- MarkerClusterGroup replaces direct marker.addTo(map)
- Custom cluster icon: red/orange gradient circle with count
- maxClusterRadius: 60px, spiderfyOnMaxZoom: true
- MarkerCluster CSS loaded from CDN in index.html

## 8. DM phishing warning banner
- PhishingWarningBanner component in DirectMessagesPage
- One-time dismissible (localStorage key: dm-phishing-warning-dismissed)
- Message: SkateSpots will never DM you asking for keys/payments
- Uses useLocalStorage hook, Shield icon, X dismiss button

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 14:03:15 +07:00
shakespeare.diy
0451887a17 feat: extended community stats page with Nostr growth data
New useGrowthStats hook:
- Queries wss://goskatespots.nostr1.com directly (authoritative source)
- Fetches all 7 content types in parallel: spots, shops, events,
  coaches, filmers, editors, tours
- Deduplicates addressable events (keep newest per pubkey+kind+d)
- Buckets events by month with gap-filling for continuous X axis
- Computes cumulative spots line chart data
- Extracts top 10 mappers by spot count
- 15 min stale time — stats don't need to be live

New useGeyserStats hook:
- Queries Geyser GraphQL for supporter count + total sats raised
- Reuses same API already used in SupportersPage

Rewritten CommunityStatsPage:
- Section 1: 8 hero stat cards (spots, shops, events, coaches,
  filmers, editors, tours, mappers) with per-type accent colours
- Section 2: 4 charts (cumulative spots area, monthly stacked bar,
  active mappers bar, content mix donut)
- Section 3: Top 10 mappers leaderboard with avatars + 🥇🥈🥉
- Section 4: Supporters banner with live Geyser count + sats raised
- Footer: data source + verifiability note
- Full skeleton loading for each section independently
- Error state for relay connectivity issues

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 12:54:32 +07:00
shakespeare.diy
0e5abcb2db fix: guard against undefined metadata in AccountSwitcher
user.metadata can be undefined when the profile hasn't loaded yet.
Changed all .metadata.x accesses to .metadata?.x to prevent crash.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 11:39:32 +07:00
shakespeare.diy
bd3e4bee4e perf: lazy loading, faster timeouts, better caching, chunk splitting
- AppRouter: lazy load all 40+ pages (only Index is eager)
  → initial JS bundle no longer includes code for pages never visited

- MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense
  → Leaflet only downloads when user navigates to /map

- useSkateSpots/useShops/useMarketplace/useTours:
  → timeout: 30s → 8s (fail fast, show error sooner)
  → staleTime: 30s → 5min (trust cache, fewer relay round-trips)
  → gcTime: 5min → 30min (keep data in memory longer)
  → refetchOnMount: 'always' → true (use cache when data is fresh)

- Remove console.log/warn/debug spam from production hooks

- vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin
  libs, radix-ui, react-router — each chunk cached independently
  by browser, only re-downloaded when that library updates

- vite.config: esbuild drops console.log/warn/debug in prod build

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 11:32:24 +07:00
shakespeare.diy
3cba958437 New logo: map pin on top of skateboard deck, same gradient colors
- Redesigned icon.svg: skateboard deck shape (rounded nose/tail, truck axles,
  wheels) beneath the existing map pin, all using the same red→orange→lime
  gradient as the original
- Replace MapPin component icons in desktop header, mobile header, and footer
  with the actual icon.svg image so the new logo shows everywhere

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 00:46:05 +07:00
shakespeare.diy
fad3a3323d Replace Impact with Bebas Neue + Barlow Condensed for authentic skate typography
- Install @fontsource/bebas-neue and @fontsource/barlow-condensed
- Add .font-display (Bebas Neue) and .font-condensed (Barlow Condensed) CSS utilities
- Hero headline: split into two lines SKATE / SPOTS with clamp() fluid sizing,
  Bebas Neue replaces Impact throughout
- Subtitle: Barlow Condensed 700 with wide letter-spacing
- Description: Barlow Condensed italic
- All section headings (Featured Spots, Events, The Lab, CTA, New Here?): Bebas Neue
- Section subheadings: Barlow Condensed with tracked uppercase
- Desktop + mobile header logo: Bebas Neue
- Footer logo: Bebas Neue
- Tagline badge: Barlow Condensed 600

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-18 00:37:32 +07:00