Commit graph

446 commits

Author SHA1 Message Date
SkateSpots
c794d5fd18 Give Lab profiles a per-craft URL: /lab/<craft>/<pubkey>/<id>
Every provider lived at /coach/<pubkey>/<id> regardless of craft (a barber got
a "coach" URL). New src/lib/labRoutes.ts maps craft kind → segment and builds
the canonical path; the router adds /lab/:craft/:pubkey/:identifier (the craft
segment is cosmetic — the page still resolves by pubkey + identifier) and keeps
/coach/... working for old links. All generated links updated: offer-form
redirects, Lab cards + "Your Lab profiles", cross-links, spot/event/admin links,
the CVM client URL, the share path, and the SPA clean-link shim.
2026-10-04 11:32:15 +07:00
SkateSpots
81b5fd0fab Add a live profile preview to every Lab offer form
Filling the long form was blind until publish. LabProfilePreview renders a
faithful profile card from the current draft — title, bio, image, location,
skills, structured rate, languages/availability/payments and gallery — with a
not-published-yet note.

- Wired into CraftProfileForm (8 crafts) and the coach + filmer/editor/
  photographer/builder pages via a Preview button next to Submit.
- parseLabExtras now accepts any { tags } so a draft tag list can be previewed
  without a published event.
- i18n for the preview copy (EN/ES).

Still open: draft/autosave and a multi-step layout.
2026-10-04 11:25:58 +07:00
SkateSpots
dd437693ab Lab offer UX: post-publish moment + "Your Lab profiles" quick access
- After publishing, land with ?published=1 and show a one-time "You're live —
  share it" banner on the profile (with a Share button), dismissible.
- /lab gains a "Your Lab profiles" strip listing the viewer's own profiles,
  each deep-linking to view/edit — so multi-craft providers aren't hunting
  through the list.
2026-10-04 11:21:40 +07:00
SkateSpots
9d1a6999ea Add owner Edit + Share to Lab provider profiles
The offer → publish loop had two dead ends: a provider's own page only offered
Delete (no Edit), and there was no way to share a listing (the whole point of
announcing a service).

- Owner "Manage" card gains an "Edit profile" button deep-linking to
  /become/<craft> (prefills the existing profile). Admins still get only
  take-down, not a misleading self-edit link.
- A Share button on every provider page opens ShareProfileDialog (QR + copy +
  native share) with the service URL /coach/<pubkey>/<identifier>.
- ShareProfileDialog accepts an optional `path` to override the default
  /<npub> share URL.
- main.tsx clean-link shim now also rewrites /coach/<pubkey>/<d> to the hash
  route, so shared /coach/... links resolve on the live site.

Follow-ups (not done): live preview before publishing, draft/autosave for the
long forms, a multi-step/progress layout, and a "your Lab profiles" manage
strip on /lab.
2026-10-04 11:16:54 +07:00
SkateSpots
681eab9592 Add craft-specific detail fields with matching display sections
Per-craft freeform fields, each wired end-to-end (form + detail display) so
none are stored invisibly:

- Artist: deliverables, turnaround
- Graffiti: surfaces, max_size
- Musician: set_length, tech_rider
- Judge: travel, certifications
- Organizer: insurance, permits
- Tattoo: deposit, minimum
- Trainer: modalities
- Barber: duration
- Filmer/Editor/Photographer: turnaround, deliverables (filmer adds drone)
- Builder: lead_time, warranty

Data-driven crafts declare them via CraftProfileForm's extraFields (now with
labelKey); the bespoke pages add matching state/inputs/prefill/save.
src/lib/craftFields.ts (CRAFT_DETAIL_FIELDS) maps each tag to a display label,
rendered on the shared detail page; toggle tags render as a localized Yes.
Labels are translated (EN/ES); placeholders stay as examples.
2026-10-04 10:53:57 +07:00
SkateSpots
88a3e4d1b4 Generalize the coach profile enrichment to every Lab craft
The coach portfolio/experience/payments treatment is now a shared layer instead
of 12 copies:

- src/lib/labExtras.ts: parseLabExtras + shared option sets (languages,
  availability, structured rate, payment methods, booking fee, gallery/reel).
- components/LabExtrasFields.tsx: the self-contained form block — prefills from
  the existing event, owns its state, and reports the managed tags via onChange.
- components/LabExtrasSection.tsx: the detail block (all crafts).

Wired into CraftProfileForm (the 8 data-driven crafts) and the four bespoke
pages (filmer/editor/photographer/builder); CoachDetailPage renders the shared
section for every craft. Per-craft buying units (rateUnits) are set per craft in
becomeRoles.tsx (e.g. barber 'per cut', musician 'per gig', photographer 'per
shoot'). /lab facet filters now read parseLabExtras, so availability/languages
filter across crafts.

Coach-specific formats/levels/ages/credentials remain coach-only. Money stays
custody-free: declared methods + a booking fee paid directly to the provider.
2026-10-04 10:49:48 +07:00
SkateSpots
90cb429dc3 fix(ui): backup-reminder timing + stray X, and event spot-link layout
- BackupReminder: wait while the signup dialog is open (the key is minted
  mid-flow), so "Save your login" no longer pops before signup is finished.
  Uses a small non-reactive signup flag set by SignupDialog.
- BackupReminder: removed the custom close button — the Sheet already renders
  its own X (the sheet's close now snoozes for the session).
- CreateEventPage: the "Link to a Skate Spot" row no longer overlaps — the
  label/hint can shrink and the Select Spot button stays put.
2026-10-03 21:18:40 +07:00
SkateSpots
b285db907d feat(map): show community confidence on spot cards
- New `useBatchSpotCommunityScores(spots)` — ONE query for many spots (same
  signals/weights as the single-spot hook; counts distinct accounts, drops the
  mapper, caps per account; skip WoT/activity weighting at this scale).
- SpotCard takes an optional `community` prop and shows a small "Community"
  (green) or "Seen" chip in the footer.
- MapPage scores the visible spot cards (shops excluded — different kind) and
  passes each card its score.
2026-10-03 19:28:28 +07:00
SkateSpots
4d30240df9 feat(crew): validate & normalise website, band and social links
Website/band/social inputs stored whatever was typed, so a bare "crew.com"
wasn't clickable. Now they run through normalizeUrlInput + sanitizeUrl (bare
domains become https://), with an inline "enter a valid link" hint on website
and band URL. A non-URL social (e.g. a @handle) is kept as typed so nothing is
dropped. Matches the profile/craft forms.
2026-10-03 18:05:07 +07:00
SkateSpots
0548a7a7c6 feat(crew): tidy the create-crew "Extras" step into collapsible groups
The Extras step stacked 9 ungrouped blocks. Now:
- Identity (NIP-05, links) — open by default.
- Sasquatch AI — open when the agent is on.
- Showcase (video, sponsor, band).
- Payments & treasury (treasury + the CLINK tip jar, moved out of "Links").
- Integrations (Lab link).
- Advanced (Coordinator) — collapsed + last, so a new founder isn't asked
  about experimental plumbing.

Other fixes:
- The crew AI agent is now labelled "Sasquatch · Pro" (it runs on the same
  Pro-gated Sasquatch proxy, falling back to a free model), with an honest note.
- crew founded year is clamped to 1954..current on save and bounded in the input.
- "Skip to end" is no longer hidden on mobile.
- Removed the coordinator copy keys left unused by the previous pass.
2026-10-03 18:02:12 +07:00
SkateSpots
c93d7e4e41 feat(crew): add a coordinator "Check status" button to the crew form
For a coordinator addressed by pubkey, the form now probes it on demand (the
same useCoordinatorHealth the Settings → Coordinator card uses) and shows
Connected / Quiet / Not seen / Unreachable. Reuses the existing socialUi
.coordinator status copy; no new keys.
2026-10-03 17:41:13 +07:00
SkateSpots
3cc16d18ca feat(crew): clearer, consistent crew-creation form
- Coordinator block: shorter copy, a Remove (✕) button, and an honest
  "pointer only — real Cordn MLS isn't wired yet" note (the old paragraph was a
  wall of jargon).
- Save now blocks on a non-empty but unparseable coordinator (was silently
  dropped) instead of publishing a crew with no coordinator.
- One consistent block order for create (wizard "Extras") and edit, grouped:
  core -> squad/places -> identity (NIP-05, links, band) -> agent -> showcase
  (video, sponsor) -> treasury -> integrations (Lab link, coordinator).
2026-10-03 17:33:35 +07:00
SkateSpots
127819e3da feat(nip05): multiple handles for profiles, crews and shops
A NIP-05 name maps to one key, so "who owns it" decides where it lives:
- Crew: repeatable nip05 tags (up to 5), verified against the founder key.
- Shop: same on kind 30017, verified against the owner key (new field).
- Profile: kind-0 has ONE standard nip05; extra personal handles live in a
  separate NIP-78 record (kind 30078, d=skatespots:profile-handles) so another
  client re-saving the profile can't wipe them.

Shared pieces:
- components/Nip05Badges.tsx — generic verify-and-render chip row (any owner).
- components/Nip05HandlesEditor.tsx — shared add/remove editor with live
  verified/mismatch status; used by profile, crew and shop forms.
- CrewNip05Badge now delegates to Nip05Badges; CrewForm uses the shared editor.
- Profile page shows primary + extra handles, and each crew/shop chips its own
  verified handle.
- lib/profileHandles.ts + hooks/useProfileHandles.ts (read/publish the record).
- types: SkateShop gains nip05/nip05s; parseSkateShop parses repeatable nip05s.
- NIP.md + AGENTS.md + i18n (EN/ES) updated.
2026-10-03 17:26:59 +07:00
SkateSpots
2daa0a9832 feat(crews): support multiple NIP-05 handles per crew
A crew is our own tag convention (unlike a kind-0 profile, whose nip05 is a
single string), so it can now advertise up to 5 handles, each verified
independently against the founder key.

- parseSkateCrew: collect all nip05 tags -> SkateCrew.nip05s (normalised,
  deduped, regex-validated, cap 5); nip05 stays = first (back-compat).
- CrewForm: add/remove handle rows with a live verified/mismatch/loading status
  (useCrewNip05 per row); format validation + max-5 hint; Enter adds.
- CrewNip05Badge renders every handle; CrewHero passes nip05s.
- NIP.md: nip05 documented as repeatable on kind 35006. i18n EN/ES.
2026-10-03 17:13:26 +07:00
SkateSpots
e1a2b91241 feat(map): show the user's own Nostr avatar on the "you are here" pin
The live-location marker used the SkateSpots brand logo. It now uses the
logged-in user's Nostr profile picture (their npub's logo), falling back to the
brand logo when there is none.

- SpotMap takes an optional userPicture; createUserLocationIcon layers the
  avatar over the brand logo (CSS multiple backgrounds) so a broken/expired
  avatar URL just reveals the logo — no inline JS, CSP-safe.
- The icon refreshes when the picture loads (or changes) without moving the pin.
- MapPage passes the current user's metadata picture.
2026-10-03 16:36:43 +07:00
SkateSpots
7aeda11041 feat(stash): password strength meter + filename hint on encrypted export
- Export dialog shows a 4-segment strength meter (length + character classes)
  with a Weak/Fair/Good/Strong label as you type the password.
- Shows the filename that will be saved; encrypted exports get an
  '-encrypted' suffix so the file is self-describing at a glance.
2026-10-03 11:29:04 +07:00
SkateSpots
0b50ce684f feat(stash): password/key-encrypted backups for Export/Import
The stash export was plaintext JSON, so a leaked/shared file exposed the whole
gear list. Export now wraps the payload in a version-2 JSON envelope:

- password mode: scrypt (NIP-49 params, 2^16/r8/p1) -> AES-256-GCM, with the
  header (mode+cipher+KDF) bound as AAD so it can't be downgraded.
- nip44 mode: NIP-44-encrypted to your own key (no password to remember; needs
  the matching key to open).
- plaintext v1 exports still import unchanged.

New src/lib/stashBackup.ts (verified round-trip, wrong-password, tamper and
KDF-downgrade rejection) + StashBackupExportDialog/StashBackupUnlockDialog.
The import picker only prompts for a password on password-encrypted files.
Password mode needs no signer (Amber-friendly); 'My key' is hidden unless the
signer supports NIP-44.
2026-10-03 11:23:49 +07:00
SkateSpots
86c9f6f856 fix(reputation): skeleton loading + no empty-card flash
The marketplace Reputation tab showed a bare 'Loading...' text under
'Reviews & trade records', and TradeRecommendationsCard rendered its
'No recommendations yet.' card while still loading (then it vanished for a
self/empty view) — together they read as broken.

- Reviews/trade-records now render row-shaped skeletons while loading.
- TradeRecommendationsCard renders nothing while its query is loading when the
  count is 0 (and only shows for a non-self viewer who can add one).
2026-10-03 10:34:25 +07:00
SkateSpots
d566af851e fix: H14 double-pay guard + H1 claim provenance
H14 (NWC double-pay): after the 15s pay() timeout, useNWC now polls
lookupInvoice to confirm settlement before reporting failure; if the status
stays unknown it throws a coded error (PAYMENT_STATUS_UNKNOWN) and the callers
that used to fall through to another rail (useZaps, usePurchasePro,
PlatformFeePayment) now STOP and tell the user to check their wallet — no
automatic second payment.

H1 (spot claim): claim-from now stores the ORIGINAL COORDINATE
(30818:pubkey:d) rather than an ambiguous bare pubkey; SkateSpot parses
claimedFrom/claimReason; a claimed listing shows a 'Claimed listing' notice so
the duplicate is understandable. NIP.md updated. Full reader-side supersede is
documented as the remaining design item (a wrong rule could hide real spots).
2026-10-03 09:34:00 +07:00
SkateSpots
caa5f8c56e fix: tier 4 — offer role trust, proof replay, crew alerts, error/nav i18n
- H13 offer threads: the seller is ALWAYS the listing author (derived from the
  event pubkey), never a self-declared role tag — a non-owner can no longer
  appear as the seller with a payment pointer.
- H12 trade reputation: a preimage/deposit proof counts at most once, so a real
  payment can't be reused across attestations to inflate payment-verified.
- Crew alerts: listed crews are filtered against the user's own membership
  confirmation labels (one batched query), so a confirmed member no longer gets
  a phantom 'confirm to join' badge/notification when the crew is edited.
- Error boundaries (app + route) and DesktopNavManager are translated; the
  class boundary renders a translated functional ErrorFallback. New batch4
  namespaces errorsUi.* / desktopNav.*.
2026-10-02 23:22:13 +07:00
SkateSpots
e016ee20bd fix: tier 3 — medium bugs & hardening
- Tour sponsor URL/logo sanitized before href/src (SponsorSlot) — same XSS
  guard EventSponsors already uses.
- Official SkateSpots badge only shows when the NIP-05 actually verifies
  (NIP19Page), not on a self-asserted handle.
- Community stats reads are null-guarded on persisted-cache fields
  (monthly/cumulativeSpots/topMappers) — no more crash on an old cache.
- Overpass response no longer assumes .elements (food layer).
- user-listings query key is memoized (was Date.now() → refetch storm).
- Pro grant requires the l=pro label (was accepted with none).
- Verify card RSVP maps accepted/tentative (was going/interested → showed
  'No RSVP' on a valid ticket).
- Expired listings can't be bought via a stale/shared URL.
- Batch spot photo/status hooks include the relay suffix and use
  querySkateContent (relay-policy parity with useSpotReports).
- Offline queue requeues rows stuck in 'publishing' after a crash.
- querySkateContent/queryCanonical dedupe replaceable events by author+kind
  (not id) so a stale profile/relay list can't win.
- Route offline cache key includes the origin (was destination+mode only).
- Internal links use <Link> (MarketplacePage); EventCountdown reacts to
  language changes.
2026-10-02 22:26:46 +07:00
SkateSpots
9189fc6584 fix(trust): tier 2 — moderation, labels & data integrity (H2–H17)
- H2 useAdminModeration now fetches the 'cleared' label too, so an admin Undo
  actually propagates (newest-verdict-wins) instead of only working on-device.
- H3 admin takedowns publish the NIP-32 'hidden' label (useAdminHide) instead of
  an author-only kind-5 relays ignore; the cross-app hide is real now.
- H5 the /stats outage banner works: queryCanonical reports relay failures via
  onError, so 'partial' can be true and zeros are never presented as truth.
- H6 useShops got its own cache namespace (was colliding with useSkateShops).
- H7 listing NIP-05 is verified against the user's key, not merely self-asserted.
- H8 NIP-22 spot comments/transfer now use uppercase root tags (A/K/P); readers
  accept legacy #a; NIP.md corrected.
- H9 Lab profile edits preserve foreign  spot refs (crew link = 35006 only).
- H10 collection edits preserve foreign NIP-51 tags.
- H11 LAB_KINDS = all 13 provider kinds (was 35001-35005).
- H16 skip-to-content is a button that focuses <main> (was href=#main, which
  HashRouter treated as a route → 404).
- H17 CustomizeNavSheet is translated; useLocalStorage re-reads on key change and
  keeps stateRef in sync cross-tab.
- Backup 'confirmed' flag is now per-account (backup-confirmed:<pubkey>).
2026-10-02 21:30:01 +07:00
SkateSpots
a8b4b9fdd0 fix(security): tier 1 — money & auth (C1–C5 partial)
- C1 Marketplace payments now verify the invoice amount: usePayPaymentPointer
  calls assertInvoiceAmount(invoice, amountSats) before paying, so a seller
  can't attach a raw BOLT11 or return an inflated LNURL invoice and have an
  auto-approving wallet pay it.
- C2 NIP-17 DMs now verifyEvent(seal) in all four read paths — the seal is the
  only authentication for a message; a crafted seal could impersonate the sender.
- C3 push-service: bind endpoint ownership (reject subscribe by a different
  pubkey; only the owner may unsubscribe) and reject non-HTTPS/private/loopback
  endpoints (SSRF).
- C4 Amber callback: a bare ?event=<pubkey> is no longer treated as a login;
  the callback must match a pending get_public_key op (both callback pages).
  Also stops logging the pending op payload.
- C5a platform-fee record is now per-pubkey, so an account on a shared device
  can't inherit another's paid status.
- C5b (self-asserted featured ranking) deferred: needs a batch-verification pass
  against provider zap receipts before it can gate ordering.
2026-10-02 21:19:10 +07:00
SkateSpots
7347f874ad i18n(es): batch 4 — Lab option labels, Sasquatch static FAQ, fee tiers
- labOptions.* (430 keys): all ~50 role LABELS/DESCRIPTIONS maps in types.ts
  (coach/filmer/editor/photo/builder/artist/graffiti/tattoo/trainer/musician/
  judge/organizer/barber) + TOUR_SKILL_LEVELS; 51 shared key maps in
  labelKeys.ts; consumers in becomeRoles/CraftProfileForm, LabPage,
  CoachDetailPage, the /become/* pages, TourCard/TourDetailPage/CreateTourPage/
  EventsPage. Trick names + tool/brand nouns stay English.
- sasquatchChat.* (83 keys): the free-tier STATIC_FAQ and local-result/fallback
  chat bodies (live AI already replies in-language; prompts stay English).
- feesUi.* (57 keys): usePurchasePro receipt prose + errors, PlatformFeePayment
  fee/duration tiers (LabProUpgrade/FeaturedEventUpgrade were already done).
- New src/i18n/batch4/ fragments spread into en/es.
2026-10-02 15:57:44 +07:00
SkateSpots
e357833557 i18n(es): batch 3 part 3 — shared label maps + leftover routing/Sasquatch strings
- labelsUi.* (221+ keys): spot categories/features/types, access, shelter, event
  types, gear categories/conditions, price/listings, tours, crews,
  member-visibility, tag categories, spot status. New src/lib/labelKeys.ts holds
  the shared value->key maps (one source; the five duplicated EVENT_TYPE_KEYS
  copies now import it). spotStatus.statusTimeAgo() uses the locale date shim.
- formatPrice() now takes an optional t so FREE/Trade/Make Offer translate.
- Route helpers: skateTime/distanceReadout/maneuver descriptors/formatAge
  translated (routeUi.*), consumers updated.
- Sasquatch: PRO_TIERS labels/badges + summarizeDraft titles/lines/ctas
  translated (sasquatchUi.*).
- Lab *_LABELS/_DESCRIPTIONS maps remain English (documented out of scope).
2026-10-02 15:31:27 +07:00
SkateSpots
9357386bbd i18n(es): batch 3 part 2 — locale-aware dates & relative times
- New src/lib/dateFnsI18n.ts re-exports date-fns with formatDistanceToNow /
  formatDistanceToNowStrict / formatDistance / format defaulting to the active
  locale, plus dateLocaleCode() for native toLocale{Date,Time}String calls.
- LanguageProvider keeps the locale in sync during render, so the first paint is
  already in-language.
- Codemod: 11 date-fns imports now use the shim; ~39 native date/time calls pass
  the app locale code instead of undefined/[]/hardcoded 'en-US'. Number
  .toLocaleString() calls are deliberately untouched.
2026-10-02 15:19:38 +07:00
SkateSpots
3e6354995a i18n(es): batch 3 part 1 — routing UI, Sasquatch panel, collections, splash
- routeUi.* (123 keys): RouteDialog (incl. follow-mode banner, transit time
  picker + mode filters, itinerary legs), StopDepartures, TripItineraryCard.
- sasquatchUi.* (81 keys): SasquatchGuide, SasquatchProDialog,
  SasquatchActionCard, SasquatchAiStatusCard. LLM prompts + chat bodies
  deliberately left English.
- collectionUi.* / bootUi.*: AddToCollectionDialog example placeholders,
  SplashScreen rotating status lines + aria labels.
- New src/i18n/batch3/* fragment files, spread into en/es so parallel passes
  never edit the big literal.
2026-10-02 15:15:06 +07:00
SkateSpots
a6dd32e653 feat(card): public pass hint + shop/listing pass badges (audit items 2 & 4)
- VenueAccessCard now has a visitor branch: a public 'this venue issues access
  passes' hint (count only, never a holder list) shown only when the venue has
  active passes. Rendered for everyone on shops and gated spots.
- New shared, cached useVenuesWithPasses() hook resolves active pass coords +
  owners in one query.
- SpotCard badges shops that issue passes; MarketplaceItemCard flags sellers
  whose venue issues passes (title explains it is the venue, not the listing).
- i18n: venueAccess.publicTitle/publicDesc/badge/sellerHint (EN + ES).
2026-10-02 15:05:49 +07:00
SkateSpots
346c5e4a3e feat(card): manage access passes from the venue page
- New owner-only VenueAccessCard on shop and gated spot pages: shows active/
  revoked pass counts for that venue and links into the card studio with the
  venue preselected (/card?venue=<kind>:<pubkey>:<d>).
- RiderCardPage reads the venue search param and opens the park editor with the
  matching venue + type selected.
- i18n: new venueAccess.* keys (EN + ES).
2026-10-02 15:00:24 +07:00
SkateSpots
733dc5200e chore(card): audit cosmetics — zoom preview, translated title, drop legacy fields, clean URLs
- U2: CardPreview is zoomable (click for a full-screen overlay); used in the
  studio, 'My passes' and the public card.
- U4: the page title is translated (Rider Card / Carné de Rider) — was hardcoded
  English.
- L4: removed the legacy style/skills from RiderCardConfig + CardData (the card
  derives style/level and shows only witnessed badges).
- I2: cardVerifyUrl/cardPublicUrl are clean (/verify/<naddr>, /card/<npub>) and a
  startup shim rewrites those paths to the hash (old /#/ links still work).
2026-10-02 14:53:12 +07:00
SkateSpots
ec02e4fe71 fix(card): audit fixes — share link, dup passes, dead stats, public style, holo persist
- B1: Share's clipboard fallback copied https://…/<npub> (a path HashRouter
  ignores → homepage). Now /#/card/<npub> (native share text too).
- B4: useIssuedPasses didn't dedupe by d, so a revoked pass showed twice;
  now newest-per-d like useReceivedPasses.
- B2/L5: the redesign dropped the stats row, so Spots/Zaps/Events toggles +
  useRiderStats + spotsAdded/zapsReceived/eventsCreated/memberYear were dead.
  Removed across lib/riderCard, CardData, CanvasCardData, both pages.
- B3: PublicCardPage ignored derived style/level (config.style is '' now) — it
  now derives them from the Skater profile like the studio; holographic honors
  the saved config.
- B6: holographic wasn't persisted — added to the card config/tags/parse.
- B5: 'Save PNG' is front-only while Print is both sides → label it
  'Save front (PNG)' when a back exists.
2026-10-02 14:43:43 +07:00
SkateSpots
2a165c7aad feat(nfc): write-to-NFC-tag for cards & passes (Web NFC)
- lib/nfc.ts: isNfcSupported() + writeNfcUrl() via Web NFC (Chrome/Android),
  writing ONLY the verify URL (the same pointer the QR carries).
- NfcWriteButton component (renders nothing where Web NFC is unavailable — iOS,
  desktop, Capacitor WebView — so there's no dead button).
- Wired into: the venue's issued-pass list (issuer copies a tag per holder), the
  organiser's issued list, and the holder's 'My passes' view (+ Print).
- i18n nfc.* EN/ES. Tags are passive and clonable like a QR; trust still comes
  from the relay signature, and /verify already handles taps.
2026-10-02 14:10:09 +07:00
SkateSpots
e9dce47d46 feat(roles): owner/admin/organiser badges across pages + card studio
- New ControllerBadge (owner / admin / organiser / verified owner).
- Crew page hero: founder -> Owner, delegated admin -> Admin.
- Spot page: Owner; Shop page: 'Verified owner' (NIP-05 domain); Event page: Organiser.
- Card studio card-type chips show your role on each entity (Park=owner,
  Crew=owner/admin, Event=organiser) so it's clear what you control.
- i18n role.* EN/ES.
2026-10-02 13:30:04 +07:00
SkateSpots
a1952a31e5 feat(card): event ticket vs host + holder RSVP check on /verify
- Attendee card = 'MY TICKET', organiser card = 'EVENT · HOST' — never present a
  self-made card as organiser-issued.
- A ticket's QR now carries ?h=<holder-npub>; /verify fetches that holder's RSVP
  (kind 31925, #a = the event) and shows Ticket holder + RSVP status, so a door
  has a real check with no server.
- NIP.md + AGENTS updated; i18n EN/ES.
2026-10-02 13:04:11 +07:00
SkateSpots
84665adc8a feat(card): redesign as a 'Credential' + unify to ONE canvas renderer
Design: the card read like a spec sheet (16 competing elements, 5-6px type).
Rebuilt as an editorial credential:
- Left full-height portrait panel; right column = brand, ONE huge name, a single
  identity line (style / level / local), nip05, crew monograms + witnessed
  badges, and a clean QR verify stub. Dropped the tiny stats row, npub clutter,
  'Powered by Nostr' filler and the 'SKATER LICENSE' chrome.
- Same system applied to Park / Crew / Event fronts; typed-up back side.

Engine: the studio preview and /card/:npub now render the canvas PNG, so there
is a SINGLE renderer (src/lib/riderCardCanvas.ts) — no React/canvas drift. The
old React card components + CardQr are deleted. Holographic is now a canvas
sheen. Also fixed several fillText calls missing an x-coord.
2026-10-02 11:57:32 +07:00
SkateSpots
ce0e004a59 feat(card): customization depth — custom accent, logo, back side, pass expiry
- Custom accent colour (native picker) overrides the theme accent via
  --ss-accent in CSS + the canvas palette.
- Optional personal logo/crest (Blossom upload) shown next to the name.
- Printable back side (RiderCardBack React + drawCardBack canvas); Print renders
  both sides, and the preview + public card can flip.
- Park passes enforce valid-thru: passExpiry() drives an EXPIRED badge on the
  card and verify page; the input is now a date picker.
- Card schema -> v:3 (accent/logo/back). i18n; NIP.md + AGENTS.
2026-10-02 11:47:19 +07:00
SkateSpots
9891101452 feat(card): Card Studio redesign + drop self-claimed skills
- Two-pane studio: sticky live preview (desktop) + inspector column; mobile
  stacks preview-first.
- Actions decluttered to a single primary Share + a '...' menu (Save PNG /
  Print / Copy ID); removed the competing big buttons.
- Unsaved-changes state + manual Save changes button (no autosave, so an
  interactive signer like Amber isn't prompted per keystroke).
- Card type is now a compact 4-up chip row; the two toggle grids merged into
  ONE 'Show on card' list with live values.
- Skills: removed the self-claimed skill chips (marginal/unverifiable). The card
  shows witnessed badges (kind 1293) only; style + level are now DERIVED from the
  Skater profile (kind 35000) instead of a manual 'Skate Style' select.
- React card + canvas export updated; i18n cardUi.studio.*; NIP.md + AGENTS.
2026-10-02 11:24:51 +07:00
SkateSpots
7cafcc0dfa feat(card,review): Event pass/wristband template + Claims tab in /review
- /card gains a fourth type: Event. Renders an event (kind 31923) you organise
  or RSVP'd going/interested to (useMyRsvpCoords, kind 31925) as a passport/
  wristband: title, date/time, venue, cover, who it admits, going count.
  Derived from the event's own signed event — its QR verifies that event;
  /verify/:naddr renders kind 31923 too. React + canvas renderers; i18n.
- /review gains a dedicated Claims tab (pending private-spot ownership claims),
  replacing the banner that sat above the tabs.
2026-10-02 11:13:40 +07:00
SkateSpots
411f6e219a feat(spots): claim ownership of a private spot (NIP-05 + moderator confirm)
Private spots can't be domain-verified like shops, so ownership is a gated
two-step flow — never an instant takeover:
- A NIP-05-verified user files a claim (kind 1985 label in the separate
  org.skatespots.spotclaim namespace) on a spot marked access=private.
- A moderator approves by publishing claim-approved (moderation namespace,
  a=coord + p=claimant); surfaced in a pending-claims list atop /review.
- The claimant then finishes by re-publishing the spot with
  claim-reason=private-approved (existing claim mechanism).

Adds useSpotClaims/useApprovedSpotClaims/useAllSpotClaims/useSubmitSpotClaim,
useAdminApproveClaim, the private branch in SpotOwnership, i18n, and docs.
Non-private spots keep the abandoned-claim flow. Nothing is auto-transferred.
2026-10-02 11:06:58 +07:00
SkateSpots
391eeb9bf9 fix(ux): recoverable route error screen + smoke-test shop/spot routes
- The per-route 'slammed' screen only offered 'Go back', so a stale-chunk
  failure after a deploy trapped the user. Add a primary 'Reload' that clears
  the lazyWithRetry session flag and does a fresh reload.
- Smoke test now boots /shop/:pubkey/:identifier, /spot/…, /event/…, /crew/…
  so a broken detail route can't ship again.
- Investigation note: no reproducible slam across 11 live shops; the report
  matched a transient chunk miss from two rapid deploys.
2026-10-02 10:52:18 +07:00
SkateSpots
ad75120a43 feat(card): Crew Card template (derived from the crew's signed event)
- /card gains a third type: Crew. Pick one of your crews and render a printable
  crew card (name, logo, home turf, founded, members, spots, vibes, Open call).
- No new event: the card is derived from the crew's own signed kind 35006, so its
  QR verifies that event (the founder's key IS the signature).
- Rendered in the React card + canvas export; /verify/:naddr now renders kind
  35006 with a link to the crew page.
- i18n cardUi.crew.* / verify.kindCrew; NIP.md + AGENTS.md updated.
2026-10-02 10:42:26 +07:00
SkateSpots
e348796e05 feat(card): identity facets — crew crests, local badge, Lab role chips
- Card shows crew crests (kind 35006 memberships), a local badge (skater
  profile location/home_spot), and Lab role chips (kinds 35001-35014) — all
  DERIVED LIVE, so a card can't claim a role/crew the skater doesn't have.
- Rendered in the React card + the canvas export (print/PNG); local/crews as
  text lines, roles as blue chips.
- Per-facet show toggles in the editor; card schema bumped to v2 so the new
  flags default ON for cards saved before them.
- Also fixed: verifyUrl wasn't passed to the canvas renderer (QR fell back to
  the profile path).
- i18n cardUi.facets.* / cardUi.roles.*; NIP.md + AGENTS.md updated.
2026-10-02 10:35:06 +07:00
SkateSpots
dbb9ea98c0 feat(card): make Rider Cards real & verifiable
- Publish the Rider Card as an addressable event (kind 32271, d=rider-card) and
  hydrate the editor from it, so customization persists across devices.
- New routes: /card/:npub (public read-only card) and /verify/:naddr
  (signature check + park venue binding).
- Park Access Cards can bind to the issuer's own spot (a tag 30818:...) and the
  verify page confirms the issuer owns the venue.
- QR on the card (+ canvas export) now encodes the verify URL; the real QR
  renders in the React card too (was a placeholder / broken profile path).
- Extracted shared card visuals to src/components/RiderCard.tsx; helpers in
  src/lib/riderCard.ts; hooks useRiderCard/useRiderBadges.
- Documented in NIP.md; i18n cardUi namespace (EN/ES); smoke test covers the
  new routes.
2026-10-02 10:26:54 +07:00
SkateSpots
0b796440f1 feat(i18n): Spanish batch 2 — nav/chrome, events/shops/tours, crews/DMs, settings/auth, wallet/stash/newsletter/stats
~3,470 new keys (5,770 total, EN=ES), ~115 more files wired.
- nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets
  (sort/trust/weather), spot-type label helper.
- placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications,
  collectives, crew chat/coordinator. settingsUi: settings, auth/signup,
  onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints,
  app shop, newsletter, community stats, leaderboards, profile.
- Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
2026-10-02 10:11:06 +07:00
SkateSpots
09735375c2 feat(i18n): Spanish (Castellano) — infra + Map/Spots, Marketplace, Lab (batch 1)
- First-visit language auto-detect (Spanish devices land in es; stored choice wins).
- Sasquatch AI replies in the app language (skate trick names kept English).
- i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t()
  key missing from en/es or any en/es key drift.
- ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and
  ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace,
  trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters.
- Policy: only skate TRICK names stay English; user-generated content untouched.
2026-10-01 23:57:20 +07:00
SkateSpots
9155949fda feat(push): extend categories to crew + social; master switch tears down push
- CATEGORY_BY_KIND now maps crew invite (36229) / join-request (31969) -> crew and
  zap (9735) / reply-mention (1) / comment (1111) -> social; the relay watch
  includes those kinds. Reactions/reposts are deliberately not pushed (noisy) and
  newsletter has no push channel.
- Master switch off now also disables web push and the native poller.
- Verified: a crew-only token is not woken by a zap but is by a crew invite.
2026-10-01 20:20:10 +07:00
SkateSpots
4df961b015 feat(push): category-aware notifications (trades-only actually means trades-only)
- /subscribe + /native/poll/register carry the enabled categories; the server
  stores them per subscription/token and only pushes matching ones (DM wrap ->
  dms; offer/counter/accept/attestation/shipment -> trades). Absent = legacy
  (all), empty array = none.
- /pending uses a per-category activity breakdown so it only moves for the
  token's categories; the smoke test proves a DM does NOT wake a trades-only token.
- usePushNotifications/useBackgroundDelivery send categories (from device prefs)
  and expose resubscribe(); NotificationSettingsCard re-sends on toggle change.
2026-10-01 20:01:07 +07:00
SkateSpots
3ad214683a feat(android): native background poller plugin (no Google) - Cordn pattern
packages/skatespots-background: a local Capacitor Android plugin that polls our
own push-service /pending and raises local notifications. Modes: off / standard
(WorkManager ~15 min) / fast (foreground service ~5 min). Kotlin +
AndroidManifest (POST_NOTIFICATIONS, FOREGROUND_SERVICE_DATA_SYNC, WAKE_LOCK,
battery-exemption) + Gradle module. Client: useBackgroundDelivery (registers a
poll token via NIP-98, seeds the plugin) + a Background delivery (Android)
control in NotificationSettingsCard. The app registers by NAME, not by importing
the package, so the web bundle is unaffected. UNVERIFIED - needs cap sync + a
device.
2026-10-01 19:51:56 +07:00
SkateSpots
4f0416c23d feat(barter): optional good-faith deposit with a verifiable receipt
A pure swap has no invoice, so ListingTradeActions now offers a 'Good-faith
deposit' input for price_type=trade listings. Paying it stores a deposit-slot
receipt (saveReceipt(coord, r, 'deposit')); the buyer's confirm attaches it as
['proof','deposit',preimage,payment_hash] with method=trade, so a barter carries
a verifiable COST without pretending the goods were proven.
2026-10-01 18:39:30 +07:00
SkateSpots
e7c6bb1ece feat(receipts): verifiable Lightning preimage receipts (money legs)
- getPaymentHashFromBolt11 (bolt11.ts, dependency-free; matches the BOLT11 spec
  test vector) + verifyPreimage (sha256(preimage) === payment_hash).
- usePayPaymentPointer captures the preimage from NWC/WebLN/melt and returns a
  PaymentReceipt; PayWithSats.onPaid carries it; stored device-locally so the
  later confirm step attaches it; publishTrade writes
  ['proof','lnpreimage',preimage,hash] (and ['proof','deposit',...] for barter).
- useTradeReputation verifies and counts it in paymentVerified.
- Honest scope: proves PAYMENT, not delivery; money legs only - a pure hardware
  swap has no invoice, stays attested; a deposit receipt gives barter real cost.
2026-10-01 17:25:16 +07:00