Commit graph

309 commits

Author SHA1 Message Date
SkateSpots
5d1a0e0fb0 feat(marketplace): listing detail page + shared trade/escrow actions
- New '/listing/:pubkey/:identifier' page: large gallery + lightbox, full
  description, details, seller trust + trade reputation card, and full actions
  (shared) for buyers and owners (mark sold/relist/delete)
- Extract ListingTradeActions (confirm/escrow request/ask-to-confirm/refund/
  contact + escrow dialog) and use it in BOTH the grid card and the detail page
  so there is one implementation
- useMarketplaceItem(pubkey, identifier) fetch hook
- Cards link their image + title to the detail page
- Smoke test covers /listing/:pubkey/:identifier

Gate: 101 tests, 0 errors/warnings, boot smoke green.
2026-09-26 22:30:43 +07:00
SkateSpots
c0aec3d912 chore(lint): reach zero warnings (react-refresh)
The last 7 react-refresh warnings are dev-only HMR DX from intentional
component+hook/helper colocations (EventCountdown, SortDropdown, TrustFilter,
LanguageContext, NewsletterPage). Splitting them into new files is broad import
churn for no runtime benefit, so each carries a justified file-level disable.

npm test: 0 errors, 0 warnings, 101 tests; boot smoke green.
2026-09-26 22:23:45 +07:00
SkateSpots
6def78fffe chore(lint): clear all no-unused-vars warnings (position-precise)
Removed/renamed the remaining 39 unused locals/dead code, file-by-file:
- unused destructured bindings/params (remove the binding, keep the call)
- unused single-line pure consts and dead functions/interfaces removed
- deleted the dead _FeedItem component and its now-unused imports
- amber _readChunks self-call fixed; SpotMap popup call site updated

Warnings: 182 -> 7 (only react-refresh/only-export-components remain).
no-explicit-any=0, exhaustive-deps=0, no-unused-vars=0.
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline + guidance updated.
2026-09-26 22:20:21 +07:00
SkateSpots
721a5d4b67 chore(lint): clear all react-hooks/exhaustive-deps warnings
Triage per file (not a blanket sweep):
- Memoise shared getters with useCallback and depend on them:
  EventsPage/LabPage/MapPage getTrustScore+getCommentCount (+MapPage getZapTotal)
- Memoise derived values that were recreated each render: CrewChat 'messages',
  RiderCardPage 'cardData'
- Justified eslint-disable for intentional mount-only effects with a comment:
  AppProvider (seed defaults/theme), SpotMap (Leaflet init once),
  SpamFeePayment (auto-pay keyed to invoice/state, not the recreated handler)
- EventCountdown: keep the intentional 'tick' recompute dep (documented disable)

exhaustive-deps: 14 -> 0. Warnings 60 -> 46 (39 unused-vars, 7 react-refresh).
Gate: 101 tests, 0 errors, boot smoke green. AGENTS.md baseline updated.
2026-09-26 22:09:43 +07:00
SkateSpots
ab87b8ab83 chore(lint): remove safely-detectable unused locals
Position-based cleanup (no name collisions) of unused catch bindings and unused
single-line pure consts across 12 files. Automated destructure/prop removals
were proven unsafe (shadowed names + typed shorthand), so those remain.

Warning backlog: 76 -> 60 (39 no-unused-vars, 14 exhaustive-deps, 7 react-refresh).
Gate green: 101 tests, 0 errors, boot smoke passing.
2026-09-26 21:58:03 +07:00
SkateSpots
d1f8870379 chore(lint): remove all explicit any + sweep dead imports
- Fix all 12 no-explicit-any (NPool, NostrEvent[], LabProfile, structural
  Cluster type, explicit SkateSpot cast) -> 0 remaining
- Remove unused imports/vars across 48 files (manual + a verified codemod for
  unused import specifiers only); tsc + tests + boot smoke all green
- Warning backlog: 182 -> 76 (remaining: ~55 no-unused-vars locals,
  14 react-hooks/exhaustive-deps, 7 react-refresh)
- AGENTS.md baseline updated
2026-09-26 21:40:42 +07:00
SkateSpots
a1c30cad4e feat(mints): Cashu mint directory (MintRadar) + same-origin proxy
New '/mints' page: browse Community MintRadar mints with health (online/latency/
degraded/archived), NUT count, terms, and add-to-wallet on native.

MintRadar has no CORS, so it's fetched same-origin at /api/mints/*:
- prod: Netlify proxy redirect in public/_redirects (+ 10m cache in _headers)
- dev: Vite server.proxy in vite.config.ts
This also hides users' IPs from MintRadar. Data is unofficial/rate-limited;
the page degrades to a friendly error state. Nav entry + smoke route added.
2026-09-26 21:20:19 +07:00
SkateSpots
15901976ef feat(marketplace): wire Cashu escrow into the trade flow (native)
Completes the escrow feature on top of the tested core (src/lib/cashuEscrow.ts):
- useCashuEscrow: native-only hook wiring createEscrow (cashu-ts mint + NWC/WebLN
  funding), redeemEscrow, escrow-key derivation (coco keyring) and profile publish
- EscrowKeyCard (Wallet): advertise the kind-0 'cashu_escrow' pubkey
- EscrowRequestDialog (listing card): buyer locks funds (P2PK to seller, refundable
  after N days) and sends the 31872 carrying the cashu_token
- EscrowPanel + 'Redeem escrow' in the DM request banner (seller side); the panel
  is offline/parse-based so it renders on web too
- Buyer 'Escrow & request' supersedes the plain request when available

EXPERIMENTAL: mint-touching paths are unverified on a device (native-only wallet);
use small amounts first. Gate: 101 tests, 0 errors.
2026-09-26 21:08:36 +07:00
SkateSpots
b91df1b4b0 feat(marketplace): trade confirmation requests (kind 31872) + fix boot smoke test
Marketplace:
- Buyer can 'Ask seller to confirm' a sold listing -> publishes a 31872
  Attestation Request (d/a/p), which the seller fulfils in one tap
- Seller sees incoming requests in the DM thread ('Confirm this sale?') and the
  fulfilling 31871 carries the spec's 'request' tag
- useTradeAttestationRequest / useIncomingTradeRequests / useListingTradeRequest
- NIP.md documents 31872 and states escrow is intentionally NOT implemented
  (coco-cashu has no P2PK/HTLC locking, wallet is native-only, and release-on-
  attestation needs a witness) so nothing looks trustless that isn't

Hardening:
- scripts/smoke-test.mjs navigated to /settings etc. WITHOUT a hash, so with
  HashRouter it only ever loaded the homepage and reported PASS. It now puts the
  route in the hash, covers settings/stash/collections/stats/marketplace, and
  fails on the 'This page slammed' error boundary (not just boot failure)
2026-09-26 20:47:29 +07:00
SkateSpots
1577ddc86f feat(marketplace): trust-weight trade reputation + in-thread trade confirmation
- Reputation now exposes per-counterparty verified counts; the profile card
  trust-weights each confirmed trade by the counterparty's web-of-trust score
  (fresh-account rings can't inflate it) and labels Highly trusted / Building /
  New trader
- usePendingTradeConfirmations finds sales a seller recorded that name you but
  you haven't mirrored; the 1:1 DM thread shows a 'Did you complete this trade?'
  banner with one-tap Confirm
2026-09-26 20:22:37 +07:00
SkateSpots
883338f060 feat(marketplace): mutual trade attestations + reputation (kind 31871)
Built on the Attestations NIP (31871); a listing is the Assertion Event:
- Seller 'Mark as sold' now opens a dialog (buyer npub + self-reported method +
  optional amount) that marks the listing sold AND publishes a trade attestation
- Buyer sees 'Confirm this trade' on the listing and publishes the mirror
  attestation -> mutual pair = a verified trade
- useTradeReputation aggregates sales/purchases/verified + counterparties
- Profile 'Trades' card; seller 'N verified trades' badge on listing cards
- NIP.md documents kind 31871 usage + our p/method/amount extensions
Payment method is self-reported metadata, never proof of payment.
2026-09-26 20:17:38 +07:00
SkateSpots
78324605c1 feat(wallet): Lightning wallet over NWC (balance, receive, send)
The Wallet page was Cashu-only and said 'App only' on web, so anyone with an
NWC wallet connected saw nothing. Adds:
- useNWC.getBalance() (millisats -> sats) and makeInvoice() via NWCClient
- NwcWalletCard: live balance + refresh, Receive (create invoice, QR, copy),
  Send (paste invoice, decoded amount shown, pay via the connected wallet)
Renders only when an NWC connection is active.
2026-09-26 20:01:59 +07:00
SkateSpots
2a47e5c9c5 feat(dm): group DMs (NIP-17) — inbox list + group thread
Uses the existing gift-wrap primitive (kind 14 rumor, one p-tag per recipient):
- useGroupConversations: lists multi-recipient threads, bucketed by the sorted
  participant key; the inbox shows a Groups section
- GroupConversationPage (/messages/group/:participants): encrypted group thread
  with sender names, send + error feedback
- smoke test now boots the new route

Group creation UI (multi-select in NewMessageDialog) is the next step. For
stronger guarantees (forward secrecy, rosters) Marmot/MLS or Cordn would be the
protocol to adopt later; noted in the hook.
2026-09-26 19:52:33 +07:00
SkateSpots
8cf35cf7db perf(bundle): entry chunk 1835 KB -> 724 KB
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.
2026-09-26 19:37:11 +07:00
SkateSpots
2edf7a797e feat(ui): adopt the shared PageHeader on Wallet, CreateListing, GearSwap, Messages
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).
2026-09-26 19:23:01 +07:00
SkateSpots
4d2c29edb0 feat(ui): PageHeader/PageContainer foundation (piloted on Contacts); profile info-first on mobile
- 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
2026-09-26 19:17:55 +07:00
SkateSpots
10ab45383d chore(ridercard): delete the unused AwardBadgeDialog
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.
2026-09-26 19:14:21 +07:00
SkateSpots
bf7fdbd621 fix(ux): calendar 'Past' opens on the newest month with events
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.
2026-09-26 19:09:21 +07:00
SkateSpots
0aee937880 fix(a11y): remove duplicate profile status dot; raise low-contrast text
- 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
2026-09-26 19:05:35 +07:00
SkateSpots
c0ee70b8bb fix(ux): map empty state; marketplace-aware CreateListing return
- 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
2026-09-26 19:03:29 +07:00
SkateSpots
760bc9b82c fix(ux): Batch F part 3 — bounty ranks, Lab tab grid, contacts search
- 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)
2026-09-26 18:54:55 +07:00
SkateSpots
a464913d6f fix(ux): Batch F part 2 — shop gallery lightbox; real FAQ content
- 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)
2026-09-26 18:52:05 +07:00
SkateSpots
617c6cee82 fix(ux): Batch F part 1 — false 'no spots' while loading; calendar filter leak
- 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
2026-09-26 18:49:14 +07:00
SkateSpots
09e1612db7 fix(a11y): Batch E part 1 — labels, valid nesting, touch targets
- 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
2026-09-26 18:46:51 +07:00
SkateSpots
dca0c0ede1 fix(ux): Batch C part 1 — Near Me timing & reachability
- 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
2026-09-26 17:58:38 +07:00
SkateSpots
8e35859188 fix(ux): Batch B — errors no longer masquerade as empty/404
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.
2026-09-26 17:51:30 +07:00
SkateSpots
cbd12f3e52 fix(ux): Batch A part 2 — stash, crew invites, map marker
- 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
2026-09-26 17:47:38 +07:00
SkateSpots
f4192e2f3d fix(ux): Batch A part 1 — dead flows
- 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
2026-09-26 17:43:19 +07:00
SkateSpots
b6c33c8a9b feat(events): crowdfunding funding goals via zaps
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).
2026-09-26 17:24:42 +07:00
SkateSpots
38c99c1777 feat(profile): zap badges (achievements) card
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.
2026-09-26 17:22:06 +07:00
SkateSpots
046627b1dd feat(home): 'Top Zapped' section linking to the leaderboard
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.
2026-09-26 17:19:55 +07:00
SkateSpots
6ae4e674dc feat(leaderboards): zap leaderboards page (top spots + top supporters)
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.
2026-09-26 17:17:21 +07:00
SkateSpots
83a3e8ae03 feat(map): make 'Most Zapped' sort actually work + batched zap totals
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.
2026-09-26 17:12:14 +07:00
SkateSpots
8d8a46a73f feat(spot): full-screen photo lightbox for spot galleries
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.
2026-09-26 17:03:30 +07:00
SkateSpots
b8bf8c8534 fix(mobile): base grid column on remaining card grids + skeleton min-w-0 consistency 2026-09-26 15:24:09 +07:00
Igor Bubelov
571257e5c3 fix(mobile): stop detail-page grids from overflowing horizontally
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.
2026-09-26 15:13:59 +07:00
shakespeare.diy
ef86d858c2 fix: make relay-probe timers and effect hooks lint-clean across ESLint versions
- 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>
2026-09-24 19:55:38 +07:00
shakespeare.diy
01d35823cc a11y: announce live updates for notifications, wallet balance, DMs (U3)
Add aria-live="polite" to the three dynamic surfaces screen-reader
users otherwise miss:
- Notification bell unread-count badge (with an "N unread notifications"
  label so the number is announced meaningfully, not read as bare digits)
- Cashu wallet balance value (announces the new balance after a
  send/receive completes)
- Direct-message conversation thread: role="log" + aria-live so
  incoming messages are announced as they arrive

ToastViewport already announces via the Radix default and was left
untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 19:39:09 +07:00
shakespeare.diy
46cb2266ea fix: restore accidentally-removed cn import in LabPage and EventsPage
The empty-state migration removed the `cn` import from both files on the
mistaken belief it was unused. In fact each file still uses cn() ~22 times
(ProfileCard, FilterDrawer, event cards, etc.), so /lab and /events crashed
at runtime with "cn is not defined" and hit the route error boundary.
Re-added `import { cn } from '@/lib/utils'` to both.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 19:07:01 +07:00
shakespeare.diy
a23225cad4 ux: migrate more empty states + fill in missing page titles (U1/U2)
Empty states:
- LabPage and NotificationBell now use the shared <EmptyState> component.
- LabPage's logged-out "Log in to create a profile" prompt preserved via
  the hint slot; removed now-unused cn import.

Page titles (U2):
- Audited all page routes for document titles. Only 3 were missing:
  - EditTourPage: now sets "Edit <tour> - SkateSpots".
  - AmberCallbackPage: now sets "Signing in with Amber - SkateSpots".
  - NIP19Page ProfilePage: sets "<name> - SkateSpots" with about as
    description; other NIP-19 branches already delegate to NotFound /
    NewsletterIssueView which set their own titles.

ScrollToTop (U1): verified already correct — resets window scroll on every
pathname change, wired in AppRouter. No change needed.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 18:50:25 +07:00
shakespeare.diy
16e943d4ef refactor: consolidate become-* routes + migrate empty states to shared component
Empty states (P4 follow-up):
- CrewsPage and EventsPage now render the shared <EmptyState> instead of
  their own local copies, keeping spacing/actions/footer consistent.

Become-role routes (P1):
- Add src/lib/becomeRoles.tsx: single registry mapping each role slug to
  either a CraftProfileForm config (artist/graffiti/musician/judge/tattoo/
  trainer) or a lazy bespoke page (coach/filmer/editor/photographer/
  builder/skater). The 6 craft roles were already thin config wrappers, so
  their configs move into the registry and the wrapper files are deleted.
- Replace 12 hardcoded /become-* routes + 12 imports with one
  /become/:role route (BecomeRolePage) that renders from the registry.
- Legacy /become-<slug> URLs now redirect to /become/<slug> so shared/
  bookmarked links keep working.
- Update all internal links (LabPage, Index, SkatersPage, SpotDetailPage,
  NIP19Page, SkaterDirectoryCard) to the canonical /become/<slug> paths.

Note: the 5 large bespoke forms were intentionally NOT merged into one
config — they carry distinct machinery (SpotPicker, wallet/Pro, geohashing)
and forcing them together would increase complexity, not reduce it.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-24 18:44:53 +07:00
shakespeare.diy
52dc0adbc7 Refine onboarding: collapse "Why bother?" for a lighter first view; update Suan Luang Backyard tagline
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-16 10:25:47 +07:00
shakespeare.diy
76411f1666 Add Suan Luang Backyard to Supporters friends section
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-16 10:20:03 +07:00
shakespeare.diy
46fbb0df43 Map loading: replace fuzzy ghost-pin blobs with a clean top bar + pill
The cold-start loading state scattered fuzzy pink "ghost pins" across the map
(the same blob look that was disliked on real markers) which read as broken.
Removed MapSkeletonPins entirely.

New loading state, per the agreed plan (keep the map instant, never cover it):
- A slim animated top progress bar ("ss-loadbar" sweep on a faint track) that
  sits above the map while spots stream in.
- During a genuine cold start (no markers yet), a small labeled "Loading spots"
  pill with a spinner near the top — honest about what's happening (querying
  relays), not fake pins on the ocean.
- Reduced-motion respected (bar becomes static).

The map itself is untouched and still renders immediately; only the placeholder
changed. Dropped the now-unused MapSkeletonPins import. Build clean, no console
errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-14 09:54:07 +07:00
shakespeare.diy
345af97788 Map Pass 1: crisp pins, spot-type & shelter glyphs, floating loading, micro-interactions
Visual foundation for the map (ideas #2, #3, #4, #6 — features #5/#7 intentionally
deferred to a later pass):

#2 Crisp pins, not blobs:
- Reworked createSkateSpotIcon — solid neon teardrop with a TIGHT glow halo
  (inset dark ring + focused shadow) instead of the soft box-shadow that read as
  a fuzzy cloud on light tiles. Cluster badges given the same crisp treatment.

#3 Marker = spot type (a readable language):
- Category emoji inside each pin: 🛹 street / 🏞️ park / 🔧 DIY / 🏛️ plaza / 🌊 transition.
- Covered/indoor spots get a cyan roof "cap" so dry spots stand out at a glance —
  the payoff of the shelter data living on the map, not just a badge.
- createSkateSpotIcon now takes an options object (weatherColor/weatherIcon/
  category/shelter); all call sites updated. Weather-layer colouring unchanged.

#4 Less scrim, floating loading:
- Suspense map-load fallback dimming reduced (bg-black/50 → /20).
- "Finding spots" pill moved from dead-centre (top 46%) to a slim top pill so it
  never blocks the map where the spots are.

#6 Micro-interactions (CSS, GPU-friendly, reduced-motion aware):
- New pins drop in with a spring; hover/focus/tap lifts the pin and pulses its
  neon halo once.

Build clean, no console errors. No tile/theme change — the "og" light map is
untouched per request.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 18:46:15 +07:00
shakespeare.diy
a7b7e07e86 Add spot "Shelter" (weather protection) field
Skaters need to know if a spot is roofed/indoor so weather info isn't
misleading — a covered or indoor spot is still skateable in the rain. Added a
4-way shelter level: outdoor (default) / partial / covered / indoor.

Data model (types.ts + skateTags.ts):
- SkateSpot.shelter parsed from a `['shelter', value]` tag (validated to known
  values; absent/unknown → treated as outdoor).
- shelterOptions / DEFAULT_SHELTER / SHELTERED_VALUES + ShelterType exported.
- SHELTER_INFO display map (emoji/label/short/color) for badges.

Publishing (CreateSpotPage + EditSpotPage):
- New "Shelter" selector reusing the exact selectable-Card pattern used by
  Access Type / Spot Type (cn() borders, neon ring, corner check badge).
- Emits `['shelter', value]` always, plus a queryable `['t', value]` for
  sheltered spots (indoor/covered/partial) so "dry spots" filtering + Sasquatch
  search work at the relay level. Outdoor adds no t tag.
- EditSpotPage initialises from the existing spot and preserves the value.

Display:
- Shelter badge on SpotDetailPage, SpotCard, and the MapPage popup — only shown
  for sheltered spots (outdoor stays unbadged to avoid clutter).

Docs:
- NIP.md: shelter added to the optional-tags table, a dedicated Shelter section
  explaining values + the queryable-t-tag behaviour, and an indoor example.

Build clean, no console errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 11:38:43 +07:00
shakespeare.diy
5506344893 Make Sasquatch tell the truth: direct relay queries, not a CVM server
The chat's data layer (useCVMClient) queries Nostr relays directly from the
browser — the CVM/ContextVM server round-trip was removed earlier for speed.
But the UI and system prompt still claimed data came "from the CVM server",
which is no longer true. Aligned every user-facing claim with reality:

- System prompt: reframed "CVM Tools / MUST route through CVM" as "Live data
  tools that query the Nostr relays directly". Tool-calling behaviour unchanged.
- Streaming status: "Querying CVM for X" → "Searching the relays for X".
- Tool-failure message: no longer blames a non-existent "CVM server"; now
  correctly frames it as relays being slow, with a retry nudge.
- Empty results: stop asserting "not added yet" as fact — an empty result can
  also mean slow relays or a misspelled place. Now offers retry + add-a-spot,
  and names the place searched. Applied to the local formatter, the system
  prompt rules, and the second-pass AI instruction.
- Chat footer: "Powered by ContextVM & Nostr Relays Data" → "Live data from
  Nostr relays" (links the primary community relay).
- About page credit: "AI powered by ContextVM" → "Trust & crew chat via
  ContextVM" (its real role: Relatr trust scores + encrypted crew chat).
- Owner CVM health panel: clarified it probes the standalone MCP server and
  that the in-app chat does not depend on it.
- Updated stale internal comments in SasquatchGuide + CreateSpotPage.

ContextVM references that ARE accurate (Relatr trust scores, crew-chat Cordn
coordinator) were left intact. Build clean, no console errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 00:16:59 +07:00
shakespeare.diy
5d96f8dfa8 Add tour countdown badges + fix/standardize NIP-05 provider prompts
Countdown badges (upcoming tours):
- New getCountdown() in TourCard: "In 3 days" / "Tomorrow" / "In 2 weeks" /
  "Happening now" (pulsing) based on start/end dates, category-tinted
- Shown on both compact and full TourCard, and in the TourDetailPage schedule card

NIP-05 provider fix (removes obsolete nip05.social):
- New single source of truth src/lib/nip05Providers.ts with working, curated
  providers: nostrcheck.me + nostr.directory (free), Nostr Plebs + nostr.band (paid)
- New reusable Nip05Required component (full-page "gate" + inline "banner"
  variants) listing those providers and deep-linking to /settings?section=nip05
- Wired into CreateTourPage (gate), CreateListingPage (banner) and all four
  Become* pages (Coach/Filmer/Photographer/Builder) for a consistent prompt
- SettingsPage NIP-05 screen now reads from the shared provider list
- Updated internal planning docs to match

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 22:31:44 +07:00
shakespeare.diy
1c295e96e2 Improve Create Tour: consistent schedules, calendar export & punk redesign
- Add a unified Schedule section across all four tour categories:
  - Local: choose a fixed session (date+time) or flexible/by-appointment
  - National/International: required departure + optional return trip dates
  - Destruction: existing competition window, now with end>=start validation
- Reuse start-date/end-date tags for every category (parser already supported them)
- Fix datetime handling: dedicated toDateInput/inputToSecs helpers so date and
  datetime-local inputs round-trip correctly (previous mix of toLocalInput +
  type="date" was inconsistent)
- Consistent, logical mandatory-field validation with clear required markers
- Redesign the header into an immersive, category-tinted, grunge-striped banner
  and rebuild the category selector as elegant glowing cards
- New calendar utilities (.ics + Google Calendar) and AddToCalendar component
- TourDetailPage is now fully category-aware (badge, guide label, duration,
  SEO title) with a Schedule card + Add to Calendar for attendees
- TourCard shows the schedule for all categories (time for local sessions)
- Update NIP.md to document start-date/end-date across categories

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 22:16:16 +07:00
shakespeare.diy
a984084464 Instant cold-start: query persistence, optimistic geolocation, skeleton pins
Three UX wins so the app feels "already loaded" the moment it opens:

1. IndexedDB React Query persistence (lib/queryPersister.ts)
   - PersistQueryClientProvider rehydrates the last-seen spots, shops,
     events, tours, crews, coaches, listings and author profiles from
     IndexedDB on cold start, so returning users land on a populated map
     instead of a blank one; fresh data refetches quietly in the
     background.
   - Strict allow-list / deny-list: only public, JSON-serializable,
     cold-start-relevant queries are persisted. Private/encrypted data
     (stash, DMs, contacts, wallet/nwc/cashu/bitcoin, signer) is NEVER
     written to disk. 24h maxAge + version buster keep it fresh & safe.
   - Pinned @tanstack/react-query + query-core + persist-client to a
     single 5.102.8 to avoid a query-core mismatch (partialMatchKey).

2. Optimistic geolocation (lib/lastLocation.ts)
   - getCurrentPosition now caches every successful fix.
   - The map opens ALREADY centered on the user's last-known area
     (city zoom) instead of the world view; "you are here" + proximity
     sort seed from the cache instantly.
   - "Near Me" flies to the cached location immediately on tap, then
     refines to the fresh GPS fix. Silent background warm-up on web map
     mount so a later tap resolves instantly.

3. Cold-start skeleton (components/MapSkeletonPins.tsx)
   - When the map is genuinely empty + loading, show elegant pulsing
     ghost pins + a "Finding spots" pill instead of a blank map; they
     vanish the instant real (or cached) spots render. Splash now
     signals content-ready when spots actually resolve.

Respects prefers-reduced-motion throughout. Build + runtime verified
clean (no console errors).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 21:58:22 +07:00