Instead of wiring encrypted crew chat into the global notification bell — which
would mean background-decrypting every crew's messages to power a badge (slow,
battery-hungry, unreliable with NIP-17's random timestamps) — add a subtle
unread dot to the crew's own Chat tab.
- useCrewChatUnread: member-only, single-crew, runs only while the crew page is
open. Uses a pure metadata query (newest Cordn envelope by created_at, no
decryption) vs. the local last-seen timestamp. For the built-in NIP-17
transport (random timestamps, needs decryption) it deliberately returns false
rather than fire false pings — the in-chat "New" divider already covers
"what did I miss" once the tab is opened.
- Tabs made controlled; the dot is suppressed while the Chat tab is active, so
it clears the moment you look, and is gone on the next visit (last-seen
advances). No global notification noise.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Skaters can now request to join a Lab collective (studio/build team), mirroring
the crew join-request flow:
- New addressable kind 36387 (collective join-request), documented in NIP.md
- useCollectiveJoinRequests: send / incoming / "already requested" hooks
- Owner-only notification surfaced in the bell (All + Crews tabs) via
CollectiveJoinRequestCard, reusing the synced dismissal set
- "Request to join this collective" button on collective Lab profiles
(RequestToJoinCollective), hidden for the owner and existing members
Trust model preserved: only the owner can edit their own profile roster, so the
card links them to the member editor — no forged membership possible.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew<->collective links existed on both sides (crew.labRefs, profile.crewRef)
but were unverified one-sided claims. Now:
- CrossLinkCards resolve the other entity and show a green "Verified" badge only
when BOTH sides link each other (crew lists the Lab profile AND the profile
lists the crew). One-sided claims show a plain link — never false trust.
- LinkedCrewCard / LinkedLabCard take a selfCoord; CoachDetailPage and
CrewDetailPage pass their own coordinate so the check works.
- CrewLinkField + LabLinkField now explain the mutual-verified behavior, so the
feature is discoverable. Both fields already restrict to the user's OWN
entities, so a verified link genuinely requires one owner to confirm both sides.
- Completed LabLinkField TYPE_LABEL for all crafts (artist/graffiti/tattoo/
trainer/musician/judge no longer fall back to "Lab").
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New PaymentReadinessNote component: checks the logged-in user's Nostr profile
(kind 0) for a Lightning address / Silent Payment and either confirms "you can
get paid" or links them to Edit Profile to add one. Wired into CraftProfileForm
(coach, filmer, editor, photographer, builder, artist, graffiti, tattoo,
trainer, musician, judge) and the standalone coach/filmer/editor/photographer/
builder forms, alongside the existing CLINK (noffer) field. Lightning address
stays a single source of truth on kind 0 — never duplicated/stale per event.
- become-judge: removed the "Downhill / Longboard" event format (not part of the
skateboarding activities this app covers).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The chips (ANYTIME / TODAY / THIS WEEKEND / THIS WEEK / THIS MONTH) ran off the
right edge on mobile and got cut off. Now on phones they render as a 5-column
grid that fits the screen exactly — with compact labels (Any / Today / Wknd /
Week / Month) so nothing truncates. Desktop keeps the full-label flex row.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The mobile toolbar stacked 5 rows of controls (title, view tabs, upcoming/past
sub-tabs, search+city, date chips) before any card showed. Tightened for phones
without removing any feature or touching desktop:
- Toolbar vertical padding + section spacing reduced (py-2/space-y-2 on mobile)
- View tabs and Upcoming/Past sub-tabs use py-1.5/py-1 on mobile
- Date-window chips: smaller top margin + padding
- Results grid top margin reduced on mobile
- "N results" count line hidden on mobile (pure noise on a small screen)
All changes are md:-gated, so desktop is unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Audited the whole events flow end-to-end (create, hosting dashboard, discovery
grid, detail, RSVP, sponsors) and confirmed the wiring is coherent:
- Create → parse → display round-trips correctly for sponsors, tour/spot/crew/
musician links, entry fee, and featured listings
- Sponsor parser safely skips shop-coordinate `sponsor` tags (no name at pos 2),
so events and tour/shop sponsorships never collide
- Batch RSVP counts feed the grid; hosting dashboard + detail keep per-item counts
Fix surfaced by the audit: CreateEventPage let a host pick a start time in the
past, silently producing a dead event in the Past tab. Added a `min={today}` on
the date pickers and a submit guard (10-min grace) with a clear message.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The filter toolbar was already sticky (stays put while cards scroll under it).
This reclaims the wasted vertical space above it on phones: smaller title,
reduced top padding + margins, and the subtitle is hidden on mobile. Desktop
layout unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
UX (all participants):
- EventCard now shows a live status pill (In 2 days / HAPPENING NOW / ENDED) on
both the full and compact variants — the at-a-glance signal was previously
detail-page only
- EventCard shows a "Presented by <sponsor>" ribbon so paid sponsor placements
get real visibility in the discovery feed (not just after a click)
- Added a bottom legibility gradient so overlays stay readable on any cover
FIX (crash risk): EventsPage getTrustScore did `trustScores?.map.get` — since
useBatchTrustScores returns a plain Map (no `.map`), this threw TypeError once
scores loaded. Now reads the Map directly.
PERF (N+1 query storm): rendering the grid fired one RSVP query PER card
(~100 queries on a full page). Added useBatchEventRsvps — a single #a-filtered
query for all event coordinates — and thread goingCount into EventCard as a prop.
Cards without the prop (detail page, hosting dashboard) keep their own query.
Removed now-dead getRelativeTime/relativeTime/isUpcoming (superseded by the
EventCountdown pill).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- FIX: EventCountdown label never updated (memo ignored the interval tick) — now recomputes every 30s for both EventCountdown and useEventStatus
- Add loud cancelled / rescheduled banner on EventDetailPage derived from host updates; close RSVP + dim hero when cancelled so attendees don't show up to a dead jam
- Clean up non-standard NIP-52 `a` tags (['a', coord, '', ''] → ['a', coord]) in RSVP and host-update publishing
- Correct misleading kind comment in useEventRsvps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Per-event og:title/description/image/url + twitter:summary_large_image via useSeoMeta
- Share description composes date + location + blurb; falls back to site card image
- handleShare now uses the canonical /#/event/... URL, richer text, and toast feedback on clipboard copy
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Immersive EventDetailPage hero: full-bleed cover with legibility gradient,
bold uppercase title, type pills, host/music-act chips, and a 3-up meta bar
(date block · time · where). Past events render dimmed.
- EventCountdown: live status pill — "IN 3 DAYS" / "IN 2H 14M" /
"HAPPENING NOW" (pulsing) / "ENDED", refreshing every 30s.
- Actions (edit/calendar/share) moved into a clean glass overlay on the cover.
- Event sponsors: EventSponsor type + parse/serialize (mirrors TourSponsor),
EventSponsors "Presented by" strip (title sponsor hero row + logo grid),
sponsor editor on CreateEventPage, and sponsor preservation on edit.
- Document event sponsor tag in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust"
signal (reuses batch trust scores); replaces the plain avatar stack
- Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only
composer with update/reschedule/relocation/cancelled categories that p-tags
every RSVPer so they get notified; public feed on the event page
- EditEventPage: auto-post a reschedule/relocation update to notify attendees
when the host changes time or location
- Activity bell: new 'event-update' notification type for RSVPers
- EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the
user's upcoming/past events, live RSVP counts, and quick edit/update actions
- Document Event Update kind 7103 in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New src/lib/routing.ts: keyless OSRM client, travel modes, distance/skate-time formatters
- New RouteDialog: self-contained Leaflet map drawing the route from the user's
location to a spot/shop/event, with a mode toggle and distance/time summary
- Wire the "Directions" buttons on Spot/Shop/Event detail pages to route in-app
instead of opening Google/Apple Maps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Audit finding: the crew form was one 1,100-line endless scroll with ~15
sections in a punishing order (logo dead-last, MLS chat coordinator sitting
inline with the crew name), only one validation gate, and no sense of
progress or what's optional.
Fix — split the flow by intent:
- Create mode: a guided 4-step wizard with a sticky progress bar, per-step
hints, Back/Continue + "Skip to end", and a sticky footer nav.
1. The Basics — logo, name, about, home turf (all you actually need)
2. Identity & Vibe — vibe tags, crew type
3. Squad & Spots — members/roles, home spots, open call
4. Extras — treasury, video, sponsor, links, band, NIP-05,
agent, and the advanced chat coordinator (reframed
in plain language, clearly skippable)
Only the crew name is gated, and only at step 1. Clickable step dots allow
jumping back freely.
- Edit mode: unchanged UX intent — renders every section flat on one page
(power users want it all at once).
All field state and the entire publish/handleSubmit logic are preserved
verbatim; sections were extracted into composable blocks with zero behavior
change. Softened the coordinator copy and the map-pin section so a first-timer
isn't scared off.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Part A — Sponsor picker:
- CrewSponsorPicker searches the shop directory and emits a 30017 coordinate
so founders select a sponsoring shop instead of pasting a raw coordinate.
- Wired into CrewForm, replacing the raw coordinate input.
Part B — Notifiable join-requests (kind 31969):
- New addressable join-request event p-tagged to founder + admins, making the
request→approve direction symmetric with invites (surfaced in the bell, not
buried in a DM inbox).
- useCrewInvites: useSendJoinRequest, useIncomingJoinRequests,
useApproveJoinRequest (founder adds member), useDismissJoinRequest (local).
- CrewRequestToJoin now publishes the structured event.
- JoinRequestCard (Add to crew / Dismiss) wired into NotificationBell and
counted in the unread badge.
- Documented kind 31969 in NIP.md alongside invites.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew coordinator is no longer just a pointer — when a crew configures one,
the Chat tab runs through it for real.
- src/lib/cordn: clean transport/crypto split so an MLS (ts-mls) upgrade is a
drop-in swap of the crypto layer only.
- types.ts: CordnMessage/CordnEndpoint + envelope kind 4497 + app tag.
- crypto.ts: sealGroupMessage/openGroupMessage — real NIP-44 group encryption
(per-recipient sealing), the exact seam MLS encrypt-to-epoch slots into.
- client.ts: buildEnvelope/publishEnvelope/fetchGroupMessages with per-group
cursor ordering and ciphertext→author sender binding (anti-spoof).
- endpoint.ts: resolve a crew's `coordinator` to a usable relay endpoint
(wss/ws direct; https falls back to the ContextVM relay).
- useCordnChat: fetch-then-poll hook mirroring the NIP-17 chat API.
- CrewChat: uses Cordn when the crew has a coordinator, NIP-17 fallback
otherwise; header badge shows the active transport (Cordn vs Private).
- CrewCoordinatorCard: reflects that private chat is live.
- NIP.md: document envelope kind 4497, delivery (fetch-then-poll + cursors),
the nip44-group sealed content, and the honest crypto→MLS seam.
Real encryption, real coordinator routing, non-breaking (fallback intact),
with the MLS ratchet as a clearly-marked next step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate team is more than "members" — the filmer, photographer and editor are
core functions. Crews can now assign optional, non-pretentious skate roles:
- Types: CrewRole (filmer/photographer/editor/artist/manager/og) + CREW_ROLE_META
labels/emoji, `crew-role` tag parser, and `crewRoles` on SkateCrew. Roles are
additive to membership and grant NO powers (only `admin` does); a member can
hold roles AND be an admin. Only honoured for founder/listed members.
- CollectiveMembers: optional rolesByPubkey → renders 🎥 Filmer / 📷 Photographer
flex chips next to each member.
- CrewLeadership: leader + captains show their skate-role chips.
- CrewForm: per-member role toggles + a founder ("Your roles") picker; publishes
`crew-role` tags scoped to the roster.
- CrewDetailPage: builds rolesByPubkey and threads it to members + leadership.
- NIP.md: document the `crew-role` tag + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate crew is defined by its footage. Crews can now feature a headline video:
- videoEmbed.ts: safe parser accepting ONLY YouTube and Instagram URLs (full
parts on YT, clips on IG). Extracts video id / shortcode; rejects everything
else so we never render an untrusted iframe. No native/self-hosted video.
- CrewVideoSection ("The Part"): cinematic section — YouTube click-to-load
facade (poster + play button, no heavy iframe/cookies until play) and an
Instagram permalink embed, framed with skate language.
- SkateCrew gains `videoUrl` (tag `video`), validated through the embed parser.
- CrewForm: "The Part" input with live source detection (YouTube/Instagram) and
validation; publishes the `video` tag.
- Crews grid: crews with footage flex a "▶ Part" badge as a discovery signal.
- NIP.md: document the `video` tag (YouTube/Instagram only) + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Extend SkateCrew with `sponsorRef` (sponsoring shop, NIP-15 stall coord) and
`coordinator` (Cordn/MLS encrypted-chat Delivery Service pointer), plus parser.
- CrewSponsorBanner: "Sponsored by [Shop]" banner that resolves and links the
backing shop; renders nothing if the shop can't be resolved.
- CrewCoordinatorCard: surfaces the crew's private-chat coordinator (E2E · MLS),
explains what the coordinator can/can't see, shows the operator, and prompts
the leader to set one up when absent — the UI foundation for Cordn chat.
- Wire the sponsor banner (under hero) and coordinator card (sidebar) into
CrewDetailPage.
- CrewForm: add "Sponsored by" and "Private Chat Coordinator" inputs with
validation; publish `sponsor` and `coordinator` tags.
- NIP.md: document the `sponsor` and `coordinator` tags, the structured invite
kind 36229 (with accept/decline flow), and the Cordn/MLS private-chat model
and why it's the chosen fit for crews.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New CrewHero: immersive full-bleed banner with per-crew accent glow, club-crest
logo overlapping the banner, member-count crest, big team name with neon shadow,
reputation crown + live activity pulse, focus/verification chips, "Led by"
team-leader inline, vibes and bio. Actions are passed in from the page.
- New CrewLeadership card: frames the founder as "Team Leader" and admins as
"Captains" with role crests, verified ticks and quick-DM — establishing the
crew hierarchy and laying groundwork for coordinator roles.
- Rewire CrewDetailPage to use CrewHero + CrewLeadership; wire live activity via
useCrewsActivity and confirmed-member/admin sets. Removed the old flat hero
card and now-unused imports.
- Upgrade CrewsPage header into a proper hero band with gradient, glow and
stat pills.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Upgrade CrewInviteDialog with a "Send invite" tab: invite a specific npub
with live invitee preview, optional note, and founder-only "invite as admin"
toggle. Preserves the shareable-link flow on a second tab.
- Add NotificationBell to desktop + mobile headers: unread badge, popover with
incoming crew invites (accept/decline) and pending-confirmation crews
(confirm with public/crew-only visibility).
- Wire useSendCrewInvite / useMyCrewInvites / accept / decline hooks and the
useCrewNotifications aggregator into the UI.
- Membership stays unforgeable (founder p-tag + invitee NIP-32 attestation).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Move sponsors from a buried sidebar card to a prominent full-width "Backed by"
bar right under the stats strip, above the About/content grid
- Redesigned as a horizontal pill row with an Award badge + gradient/glow accent,
so reppin' brands reads as a badge of pride
- Removed the duplicate sidebar Sponsors card
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Even 2x2 (mobile) / 1x4 (desktop) grid instead of a ragged flex-wrap where
"Gear" dropped to its own row and "Member since" floated awkwardly
- Each stat now has an icon badge + prominent count + label, so the number reads
as a number (previously the tiny colored count looked like a status dot)
- Zero-value stats render in a muted, inert state — a brand-new profile now reads
as "nothing yet" rather than a wall of dead grey zeros
- Moved "Zaps received" + "Member since" into a clean secondary meta line below,
keeping the primary grid focused on the four core contributions
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- StatPill now accepts an onClick and renders as a hover-lift button when it has
a non-zero value (inert div when empty), with clear affordance
- Spots / Shops / Crews / Gear pills smooth-scroll to their matching section
(added ids + scroll-mt-24 offset so headings aren't hidden under sticky nav)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NIP-05 (the field you couldn't find — it existed but was a bare input):
- Promote to its own prominent, self-explaining row (out of the cramped grid)
- Live verification: checks the handle resolves back to your own key, with checking/verified/mismatch/unreachable states, an inline status icon, plain-language copy, and a "How?" help link
Share + QR:
- New ShareProfileDialog: QR of the profile URL (great for IRL/stickers), copy-link, and native share-sheet fallback
- "Share" button added to every profile header
At-a-glance stats:
- Compact stat strip near the top (Spots / Shops / Crews / Gear / Zaps + "Member since")
- Removed the now-redundant sidebar "Contributions" card
Cleaner empty states:
- Spots/Shops sections no longer show dead "nothing here" cards on other people's profiles — hidden entirely when empty
- Own empty Spots state is now an actionable nudge linking to /create-spot
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fields collected on the Become-Skater form were being dropped on the profile:
- Instagram: now rendered in the profile Info card (normalizes @handle / full URL, links to instagram.com), matching where website/lightning live
- Location (city/area): now shown as "Based in" in the skate-layer stats row
- hasSkateLayer now also triggers on location/instagram so the skate card renders even when those are the only fields filled
All other fields (photo from kind-0, bio, styles, stance, level/custom level, years, home spot + mapped link, website) already round-trip consistently between the two pages.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Keeps the skater directory fully open — NIP-05 is a trust signal, not a gate:
- Extract reusable NIP-05 verification into lib/nip05.ts (parse, fetch well-known, verify against expected pubkey); refactor useCrewNip05 to use it
- New useSkaterVerifications hook: batch-fetches kind-0 metadata for all skaters and verifies each nip05 resolves to its own pubkey, returning a Set of verified pubkeys
- SkatersPage: add a "Verified (N)" toggle (disabled when none verified) that filters to genuinely verified skaters; show ✓ badge only for truly-verified skaters; verified-aware empty state
- NIP19Page profile: nip05 line now reflects real verification (lime ShieldCheck + "Verified" pill when the handle resolves to the key, muted grey otherwise)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Home spot:
- Add "Browse spots" button opening a searchable dialog of real mapped spots (useSkateSpots)
- Picking a spot stores its title + an `a` (30818:pubkey:d) coordinate; shows a linked-spot chip with unlink
- Profile page renders a linked home spot as a link to /spot/:pubkey/:identifier
- Parse homeSpotRef from the `a` tag; document in NIP.md
Profile pic consistency:
- Remove the skate-specific image override/upload — the skater card now always
uses the kind-0 profile picture so both sections show the identical photo
- "Profile Pic" section is read-only with a "Change profile photo" link to settings
- Drop the `image` tag from published skater events; note the change in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Become-Skater form: photo section relabeled "Profile Pic"
- Level picker now includes a "Custom" toggle with a free-text input (≤40 chars) for labels like "OG", "Weekend Warrior", "Retired"
- Store custom level as a `level_custom` tag (mutually exclusive with preset `level`)
- Render custom level everywhere presets show: profile (NIP19Page), SkatersPage cards, SkaterDirectoryCard
- Document `level_custom` tag in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Make the skater layer and the universal Nostr profile feel like one person,
with kind-0 as the single source of truth for shared fields.
1. Public profile (NIP19Page): render the skate layer inline — stance, level,
styles, years, home spot as a branded card right under the About. Own-profile
nudge ("Add your skate details") when no 35000 exists. Website in the sidebar
now falls back to the skater profile when kind-0 has none.
2. Become-Skater: prefill bio/website from kind-0 and default the photo to the
kind-0 avatar (with "using your profile photo" affordance). Store shared
fields ONLY as overrides — matching values are dropped so kind-0 stays
authoritative and nothing goes stale. "From your profile" hints + a
linked-to-your-profile banner make the relationship explicit.
3. Fallback-on-render for website/image/bio wherever the skater field is empty.
4. Bidirectional cross-links: new SkaterDirectoryCard in Settings > Profile
shows directory status (level + style chips) linking to edit, or nudges the
user to join; Become-Skater links back to full-profile editing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New app-native Skater Profile kind 35000 with `skateskater` marker tag,
the anti-spam boundary that keeps generic kind-0 Nostr profiles out of the
directory (queried only from the community relay).
- Skater type, stance/style/level constants, and parser in types.ts.
- useSkaters + useUserSkaterProfile hooks.
- /skaters directory page: immersive hero, live count, search, style filter
chips, ranked skater grid with NIP-05 verification badges and lazy author
metadata.
- /become-skater opt-in flow (bio, photo, styles, stance, level, years, home
spot, location w/ geohash, socials).
- Wire "Skaters" into the mobile nav and a cross-link banner in The Lab so
plain skaters finally have a home.
- Document kind 35000 in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Restructure EditProfileForm into clean, titled sections (Identity,
Bitcoin & Payments, Skate Profile, Account) with gradient icon
headers, consistent spacing, and a reusable FieldRow with inline
icon + validation status.
- Sponsors is now a dynamic per-line list (add/remove rows, Enter to
add) instead of a textarea; stored newline-separated. The profile
renders each sponsor as its own badge.
- Lightning address placeholder is now generic (you@yourdomain.com) —
no specific wallet names anywhere.
- Nicer image upload: click-the-thumbnail-to-upload with hover overlay,
round avatar preview / wide banner preview.
- Sticky Save bar at the bottom of the form.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Editable in EditProfileForm and displayed on the profile:
- Bitcoin Lightning Address (lud16) — now editable in-form, shown with a
labeled Lightning row in the Info card.
- Bitcoin Silent Payment Address (BIP-352, custom `bitcoin_silent_payment`)
— validated (sp1…), shown as a copyable, truncated row.
- Sponsors & Support (custom `sponsors`) — freeform highlight, rendered in
a dedicated pink "Sponsors & Support" sidebar card.
- Cordn contact link (custom `cordn`) — private MLS messaging link
(cordn.net), shown as a "Message privately" row in the Info card.
Crews and the CLINK noffer tip button were already present, so they are
left as-is. Added isValidSilentPayment / isValidLightningAddress
validators and documented the custom kind-0 fields in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use the established "Stash" term everywhere instead of introducing a new
word. The profile section is now titled "Stash", visibility copy reads
"Shown in your public Stash on your profile", and internal identifiers
were renamed (useUserQuiver → useUserPublicStash, QuiverItem →
PublicStashItem, parseQuiverItem → parsePublicStashItem). NIP.md updated
to match.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previously the "Public" visibility on Stash items did nothing — the
whole Stash is a single NIP-44 encrypted blob, so nobody could ever see
a "public" item. This makes Public actually mean public.
Model (hybrid A + marketplace):
- Public items are now ALSO published as separate unencrypted NIP-99
kind 30402 events tagged `t: stash-showcase`, keyed by `stash-<id>`.
- Public + not-for-sale → showcase-only (`showcase: true`, price 0):
shows on the profile "Quiver", filtered OUT of the sellable market.
- Public + for sale/trade → full listing: shows on the profile Quiver
(badged For Sale/Trade) AND flows into the marketplace.
- Going private or deleting an item publishes a NIP-09 deletion for its
public event, keeping everything consistent.
Changes:
- New hook useStashShowcase (sync/unpublish + useUserQuiver query).
- StashPage wires add/edit/remove through sync handlers; honest
visibility copy in add + edit dialogs.
- Profile page (NIP19Page) renders a new "The Quiver" grid with
For Sale/Trade badges + prices, and a Gear count in Contributions.
- Marketplace hooks skip showcase-only events so they don't clutter
buy/sell listings.
- NIP.md documents the showcase event schema.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Let the title badges (Encrypted · Syncs everywhere · sats) wrap
gracefully instead of colliding with the toolbar controls
- Hide the list/grid view toggle on the empty state (nothing to toggle)
so it no longer clutters the space next to the Add button
- Give the title row room to grow on mobile (min-h-14 + padding)
instead of forcing everything into a fixed 56px row
- Make the search input flex-fill and shrink the category select on
small screens so the filter bar no longer overflows
- Add aria-labels to the view-toggle buttons
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Flesh out SkateJudge as a full craft-based Lab profile (parseCraftBase),
with JUDGE_FORMAT / SPECIALTY / LEVEL label sets covering Game of S.K.A.T.E.,
best trick, jams, contests, am series, world tour stops, etc.
- Wire judge into useLabProfiles (union type, self-tag, parse case)
- Add "judging" ServiceType to LabPage: tab, accent, filter sections, empty
state, counts, profile card icon, and ?craft=judging deep link
- New BecomeJudgePage + /become-judge route + FAB entry
- CoachDetailPage renders judge profiles (accent, icon, labels, noun, title)
- Community Stats "Judges" card now links to /lab?craft=judging
- Document kind 35012 (skatejudge) in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useGrowthStats now queries and exposes every Lab craft individually: artists,
graffiti, tattoo, trainers, musicians and judges (previously folded into an
opaque "other" bucket with no visible breakdown).
- CommunityStatsPage renders a card for all 11 Lab crafts, so the section matches
what /lab actually lists instead of only showing 5 categories.
- labTotal + the content-mix donut's "Lab" slice now sum every craft, keeping the
"total" badge and chart consistent.
Note on relay mismatch: the app dedupes addressable events (keeps newest per
pubkey+kind+d) and merges across the community relay + the user's relays, so
counts can legitimately differ from a raw single-relay browser that shows stale
duplicate versions.
Bump version to 1.2.1 (versionCode 10201).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Map filter: renamed the "Bitcoin" toggle to "Spedn" (matches the toolbar
button); tooltip now says "Spedn your sats".
2. Community stats (/stats): useGrowthStats now queries the same relay set as the
rest of the app (community relay + user relays) via useSkateRelays instead of
only the single hardcoded SKATE_RELAY. Lab profiles / spots / etc. that live on
a user's own relay are now counted, so the numbers match what /lab shows.
3. Near Me on GrapheneOS: even after granting the OS location permission, the
WebView's navigator.geolocation stays blocked → "permission denied". Fixed by
fetching the position from the native Capacitor Geolocation plugin first
(LocationManager, honours the granted permission), falling back to
navigator.geolocation only if that fails. Also auto-inject
ACCESS_FINE/COARSE_LOCATION into AndroidManifest via the manifest script.
Bump version to 1.2.0 (versionCode 10200).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewCard: rebuilt to fit any screen width — clip overflow, truncate name/location, single wrapping badge row, consistent avatar sizing; responsive header
- Amber web login: after opening Amber, drop user on manual paste step after 2.5s so the spinner never hangs when the callback opens in a new tab or clipboard is blocked; keep polling on the paste step so a late callback still auto-completes
- Stash: hasNip44/hasNip04 now fall back to the active login when pubkey matching fails, so logged-in nsec/amber users are no longer wrongly told "Encryption Required"
- Signup: added an explicit welcome step that explains account creation and only generates the keypair when the user taps "Create my account" (no more silent auto-creation on dialog open)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- signerCapabilities: recognize x-amber login type as NIP-44/NIP-04
capable (Amber implements nip44_encrypt/decrypt via native intents and
URL redirects). Fixes "Encryption Required" wall on Stash for users
logged in with Amber on Android.
- CrewDetailPage: restructure hero header so actions wrap below the
title on mobile instead of squeezing it in a cramped 3-column flex;
smaller avatar/title on small screens, truncating location.
- MapPage: remove Quick Snap from the mobile Add speed-dial and all its
handlers, refs, web-fallback input, and now-unused imports.
- zapstore.yaml: explicit orange icon + metadata_sources: [] so the
correct logo is always used on the Zapstore listing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completes the admin moderation coverage from the previous commit.
Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
admin-hidden entries (list) and 404 hidden ones (detail) via the shared
moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
card to admins too, not just the owner, with admin-specific copy.
DeleteEntityDialog already handles the owner-or-admin logic.
Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
hidden label). Report stays for non-owners.
SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Critical: Leaflet marker popups built untrusted Nostr event fields (and
BTC Map API fields) into innerHTML, bypassing React's output encoding. Any
user could publish a spot/tour/crew/event with an <img onerror> payload in a
text field; it executed on /map render with no interaction, letting injected
script read the plaintext nsec from localStorage (nostr:login) and the Cashu
seed (cashu-mnemonic) — permanent, unrotatable account + wallet takeover.
Fixes in src/components/SpotMap.tsx:
- Rebuild all popup DOM with createElement + textContent instead of innerHTML
for every untrusted field: spot.location; tour.title/location/meetingPoint;
evt.title/location; crew.name/location; BTC Map name/address/opening_hours/
phone/description.
- Validate every event/API URL with sanitizeUrl() before it reaches an
href/src: crew.image (popup + marker icon) and BTC Map website. Drop
anything that isn't http(s). tel: links reduced to safe characters.
- Replace inline onerror handler on the crew marker image (would be blocked
by CSP anyway) with a scripted img.onerror fallback.
- Add popupRow() helper so untrusted detail rows can never be innerHTML'd.
Defense in depth (web only, native Capacitor build unaffected):
- Add public/_headers with a strict Content-Security-Policy: default-src
'none'; script-src 'self' 'wasm-unsafe-eval' (no unsafe-inline/eval);
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; plus the
https:/wss: allowances a Nostr client genuinely needs for relays, media,
and mints. Also X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy.
- Move the inline boot-crash-guard script to public/boot-guard.js so
script-src can stay 'self' with no 'unsafe-inline'; index.html now loads it
externally. The guard also now paints errors via textContent, not innerHTML.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Latitude/longitude fields are now optional for local/national tours
- If provided, both must be filled and valid; otherwise geohash and
coords tags are omitted from the published event
- Updated input placeholders and added helper text clarifying coordinates
are optional
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
CreateTourPage called usePlatformFee('featuredTour') but never imported the
hook, causing a "usePlatformFee is not defined" ReferenceError that crashed
the /create-tour page. Added the import to match the other pages that use it.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The "Tip the Mapper" card now links the avatar/name to the mapper's profile
(/{npub} via the NIP-19 router) and adds a "Message Mapper" button that opens
an encrypted DM (/messages/{npub}). Makes it easy to contact whoever added a
spot — e.g. to ask them to add photos to a photoless submission. The message
button is hidden when you're viewing your own spot.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Add issue #5 (2026-08-08) to NEWSLETTER_ISSUES with a Groucho-Marx satiric
tone. Highlights: the 3x-bigger Lab (10 crafts), the Lab mobile grid fix,
Crews covering every scene with opt-in visibility + web-of-trust vouching,
the redesigned CrewDetail page, and Music<->Events interconnect. Mocks
"OpenCrack" for volunteering to fix Bitcoin's crypto libraries, and owns
that the app has never been audited (one day it will be — and we'll shoot
ourselves in both feet). Includes matching nostrText teaser for kind 1.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
On mobile the 10 craft filters (Coaches, Filmers, Photo, Editors, Builders,
Artists, Graffiti, Tattoo, Trainers, Music) were in a sideways-scrolling
strip, hiding options off-screen. Switch to a 5-column grid on phones (two
tidy rows of five) while keeping the flex-wrap layout on desktop, so every
craft is visible at a glance with no horizontal scrolling.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Step 3 — member visibility (best-of-both, honest):
- MemberVisibility ('public' | 'crew') stored on the member's OWN kind-1985
attestation via a `visibility` tag, so a public crew can contain a private
member. Confirmed crew-mates + founder see them; outsiders see anonymised
"hidden member" slots. Framed honestly in-UI (app-level discovery privacy).
- VisibilityPicker in ConfirmMembershipCard (choose on join) +
MemberVisibilityControl for confirmed members to change it anytime.
- useCollectiveMembers parses visibility; CollectiveMembers hides crew-only
members from non-members.
Step 4 — anti-fraud vouch layer:
- useVouches/useVouchFor: NIP-32 vouch labels, trust-WEIGHTED via web-of-trust
so fake-account vouch rings carry no weight. VouchCard with new/vouched/
scene-verified tiers, wired into every Lab profile detail page.
Step 4 — CrewDetail UX redesign:
- Activity consolidated into clean Feed/Events/Chat tabs (no more giant scroll)
- Sidebar reordered: Members → Support → Band → Connections → Founder
- Member visibility control surfaced; tighter hero
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New addressable kinds 35007–35011 with types, label maps and parsers
- Shared parseCraftBase helper + parseLabProfile dispatcher in useLabProfiles
- Reusable, config-driven CraftProfileForm (polished shared shell, per-craft
skill sections + extra fields, entity/collective, CLINK, Pro upgrade)
- Five thin Become* pages wired into AppRouter and the Lab "Offer" menu
- LabPage: extended ServiceType, accents, scrollable tab strip, filter
sections, counts, empty states and ProfileCard to all 10 crafts
- CoachDetailPage renders all craft types (map-driven accents/icons/labels)
plus craft-specific fields (studio, books-open, credentials, remote,
graffiti crew, music links)
- NIP.md documents all five new craft schemas
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The public profile page overflowed sideways on phones, pushing the Info /
Contributions / Crews sidebar off-screen. Cause: the About text contained a
64-char unbreakable URL (ContextVM server link) with no word-breaking, forcing
the layout wider than the viewport.
- About text now uses break-words + overflow-wrap:anywhere so long URLs wrap
- Added min-w-0 to both grid columns so they can shrink instead of overflowing
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The app had two things both called "Profile": the /settings editor and the
public /npub page. They're not duplicates (edit vs. view) but the naming and
lack of links between them was confusing. Now:
- Public profile (/npub) gets an owner-only "Edit Profile" button that deep-
links to /settings?section=profile
- Settings row renamed "Profile" -> "Edit Profile" (section title already said
Edit Profile); it already has a "View my public profile" link back
- Result: one Profile (the public page), one obvious way to edit it, linked
both directions — the standard IG/GitHub pattern
Bonus fix: mobile menu "My Data" used a broken ?tab=mydata param; corrected to
?section=mydata (the settings page reads ?section=).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Profile page (NIP19Page):
- Add "member of crew X" summary under the header (with founder crown)
- Add a full Crews sidebar card listing every crew the user founds/belongs to
- Add crews count to the Contributions stats (now Spots/Shops/Crews)
- Show a Bot badge for kind 0 bot accounts
- CLINK "Tip in Bitcoin" already surfaces on profiles (from prior work)
- New useUserCrews(pubkey) hook powering the above
Messages:
- Replace paste-only "New Message" dialog with NewMessageDialog: a searchable
people picker (crew-mates + following, grouped, name-searchable) that also
accepts a pasted npub/hex as a fallback. Starting a DM now feels like a
real contacts screen.
Map toolbar consistency:
- Consolidate secondary add actions (Shop / Gear / Tour) into a single
"+ More" popover; keep primary "Add Spot" prominent. 4 buttons -> 2 controls.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
#0 Crew band gig booking:
- Rename "Book them for a gig" -> "Set up a gig" (honest: it creates an event)
- Add "Message about a gig" -> pre-filled encrypted DM (NIP-17) to the crew
founder via existing /messages/:npub?message= flow (real communication)
#1 Profile access (was missing/mislabeled everywhere):
- Desktop header: add "View my profile" icon linking to /npub
- Mobile user menu: split mislabeled "My Profile"(->Settings) into
"View Profile" (-> /npub) + "Settings"
- Burger menu: add "View Profile" entry
- Settings > Edit Profile: add "View my public profile" quick link
#2 Remove standalone Local Chat button on desktop map toolbar (kept on mobile)
Audit fixes:
- Mobile menu "Wallet & Stash" -> "Wallet" (Stash is already its own item)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add "punk" + "rap/hip-hop" crew vibe presets
- Hide Crew Treasury behind SHOW_CREW_TREASURY flag (detail page + form), code kept
- Improve crew band section into a dedicated card with a "Book them for a gig"
action that deep-links to a Concert event hosted by the crew
- CreateEventPage: prefill event type + title from ?type= / ?title= params
- CLINK (a): expose CLINK tip code on user profiles (EditProfileForm field +
"Tip in Bitcoin" button on profile view, stored as custom kind 0 field)
- CLINK (c): relabel "Pay via CLINK" -> "Tip in Bitcoin" with a "what's this?"
tooltip explaining it in plain language
- Remove standalone desktop Messages icon (kept in the More menu)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
"Start a Crew" was ambiguous (team? chat? listing?). Renamed to the clearer
"Create Crew" across the Crews page button/empty state, Create Crew page
title/header/login gate, and Sasquatch FAQ. Added a subtitle distinguishing a
Crew (skate WITH) from The Lab (hire).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: NUser's extension signer exposes .nip44 as a getter that calls
.nostr (window.nostr) and THROWS when the extension isn't present. Any
synchronous access during render crashes the whole tree — the actual culprit
was useStash's `isEncryptionAvailable: !!user?.signer?.nip44` in its return
object, evaluated every render even when logged out.
- Add src/lib/signerCapabilities.ts: hasNip44/hasNip04 helpers that determine
capability via login type + direct window.nostr probe (no throwing getter).
- useStash: use the helper for hasNip44 + isEncryptionAvailable
- useDirectMessages: replace all render-path `enabled: !!user?.signer?.nip44`
with a pre-computed nip44Available across all 4 conversation hooks
- CrewChat, CrewRequestToJoin, ConversationPage, DirectMessagesPage,
ContactsPage: use the safe helper in render/guard paths
Async accesses (inside queryFn/callbacks) are left as-is — they reject
gracefully within react-query and never crash render.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Grew the glossary from ~80 to ~150 entries across 15 categories.
- Added many more tricks per the community list: impossible, gazelle
flip, pressure/hospital/dolphin flips, bigger spin/flip, suski/willy/
losi/hurricane grinds, japan/madonna/slob/seatbelt/roastbeef/benihana/
lien/cannonball/christ grabs, 720/1080, casper/anti-casper, spacewalk,
walk the dog, g-turn, and more — with inventor credits and aka names.
- New reference categories: "Transition & Vert Terms", "Stance &
Movement", and "Hardware & Equipment" (deck, trucks, bushings,
bearings, risers, wheel bite, etc.).
- Reference categories hide difficulty badges (added `reference` flag +
`hideLevel` on TrickRow); search still spans everything.
- Updated hero copy + SEO description to reflect the broader scope.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Skate scenes and local music go hand in hand — crews can now list a band
name + a music link (Spotify/Bandcamp/YouTube/Nostr).
- SkateCrew type: new optional bandName / bandUrl fields.
- Parser reads `band` + `band-url` tags (band-url validated as http(s)).
- CrewForm: dedicated optional "Crew Band" section.
- CrewDetailPage: purple band row (linked when a valid URL is present).
- CrewsPage cards: small "Band" badge when a crew has a band.
- Kept scoped to crews only (not The Lab) since bands aren't a
skate-specific craft.
- NIP.md: documented the new `band` / `band-url` crew tags.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Glossary:
- New /glossary page: 80+ tricks across 11 categories (fundamentals,
flips, shoves/spins, grinds, slides, grabs, airs, lip, stalls,
handplants, boneless, freestyle) with alternate names, difficulty
badges, live search, and category filters.
- Added to desktop "More" dropdown and mobile burger menu.
Sasquatch AI:
- Fixed/expanded free-model fallback list with valid OpenRouter slugs
(dropped the non-existent gemma-4 slug).
- Added exponential backoff + one retry on 429 rate limits, respecting
Retry-After headers.
- SasquatchGuide now reuses the shared sasquatchBrain caller instead of
a duplicated implementation.
- Friendlier fallback message when the AI is saturated.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Probes the ContextVM relay for the CVM server's own events and reports
online/stale/offline with a last-seen timestamp. Gated to the two
platform owner pubkeys (renders null for everyone else). Notes that
Sasquatch's data layer queries relays directly and is unaffected by the
server being down.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Option B: a lightweight reorder / show-hide list for the desktop header.
- useDesktopNavPreferences: separate localStorage-backed preference for which
catalog items appear as primary top-nav tabs and their order (Map fixed
first, no hard slot cap). Reuses the shared NAV_CATALOG.
- DesktopNavManager: settings UI to reorder primary tabs (up/down) and toggle
items between the top bar and the "More" menu; Map locked; reset to default.
- Layout desktop header now renders fixed Map + user's primary tabs, and the
"More" dropdown dynamically lists the hidden catalog items (auth-gated),
so each item appears in exactly one place.
- Manager shown only on md+ screens (mobile uses the bottom-bar customizer).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Crews page
- "Near me" geolocation toggle with haversine distance sort + distance chips
- Crew count + cities social-proof stat header
- Deterministic per-crew vibe-colour accents (no more wall of teal)
- Activity pulse ("🔥 active this week") + OG/Verified/Established rep badges
- Rich empty state ("Claim your local scene") instead of dead "No crews yet"
Crew synergies
- Crew treasury: transparent, non-custodial NIP-75 zap goal (kind 9041) with
live progress bar tallied from zap receipts; funds settle to founder's own
Lightning wallet — no shared wallet or new wallet settings needed
- Crew-hosted events: host a jam/comp as the crew (event a-tag → crew coord),
"Hosted by [crew]" on event pages, Crew Events list on the crew page
- Crew ↔ Lab hiring pipeline: "Open Call / looking-for" flags (filmer, editor,
coach, builder, members) surfaced on the crew page
- Request to join: encrypted NIP-17 DM to founder + admins (no founder-push
bottleneck, no public join event)
- Reverse links: "Crews that skate here" on spot pages
Contacts (0xchat-style + Armada-style privacy)
- New /contacts page with Crew-mates / Follows / Recent DMs / Saved tabs
- Per-contact quick actions: message, zap, view profile, star
- Private encrypted contacts list (NIP-51 kind 30000, NIP-44 encrypted to self)
- Added to burger menu + desktop More menu (gated to logged-in users)
Docs: NIP.md updated with crew treasury, crew-hosted events, looking-for tags,
and the private encrypted contacts list.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Expanded Drop #003 from 7 to 16 update cards covering everything actually
shipped this session (per git log), grouped: the geolocation war (crash
fix, parallel first-fix GPS, progress UI, you-are-here pin, crash-proofing,
no-provider messaging), Spot Status Reports (reporting, trust/recency
consensus, badges, gone-spot hiding), Quick Snap camera-first + snap→Add
Spot, Amber signing timeout + plugin fix, crew/Lab DELETION via NIP-09
(framed as the new deletion capability, not the existing features), and
native build plumbing.
Cranked the tone harder throughout and leaned fully into the "weekly
newsletter, shipped whenever the f*** we want" bit in both the vibe line
and masthead. Updated nostrText to match.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New issue covering everything shipped this session, written funny:
- Near Me crash fix (Kotlin 2.1 alignment)
- Parallel first-fix GPS strategy that works on GrapheneOS / no Play Services
- "Locating you…" / "Found you" progress feedback
- "You are here" user-location pin
- Community Spot Status Reports (active/bust/skate-stopped/closed/gone)
- Gone spots hidden from map markers
- Defensive native geolocation hardening
Also updated the masthead tagline to lean into the "weekly newsletter,
shipped whenever the f*** we want" bit. Includes ready-to-post nostrText
for Drop #003.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
After Near Me centers the map, there was no indicator of where the user
actually is. Add a Google-Maps-style location marker, brand-flavored:
- Circular badge using the SkateSpots logo (public/images/user-location.png)
with a red (#ff0040) ring, sitting at the user's exact coordinates.
- Pulsing translucent accuracy halo (ssPulse keyframes) so it reads as a
live "you are here" and doubles as subtle motion.
- Rendered above all other markers (zIndexOffset), with a "You are here"
popup. Created/moved/removed reactively from a new `userLocation` prop.
Wired MapPage's userLocation state through to SpotMap (lat/lon → lat/lng).
Marker image path uses import.meta.env.BASE_URL so it resolves on both
web ('/') and the Capacitor APK ('./').
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The persistent failure on GrapheneOS (no Play Services, GPS-only) was
caused by the acquisition strategy itself, not the OS:
- The old low-accuracy-only watch never fired without a fused/network
provider, and the 25s deadline killed it before a cold GPS lock (which
takes 30–60s on GrapheneOS) could ever deliver.
Rewrite raceForFirstFix to actually work on GPS-only devices:
- Run BOTH a high-accuracy (GPS) and low-accuracy (network) watch in
parallel — high accuracy is what engages the GPS chip and is usually
the one that delivers on GrapheneOS.
- Also fire a one-shot high-accuracy getCurrentPosition.
- Extend the deadline to 60s (real cold-GPS lock time) and classify a
timeout-flavored failure correctly.
- Ignore invalid/null-island (0,0) readings.
- Drop the redundant post-race extra attempt.
UX: show a persistent "Locating you…" toast (dismissed on success/fail)
so the up-to-60s GPS lock doesn't look frozen, plus a "Found you"
confirmation. Failure still surfaces the raw error text for diagnosis.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause of the persistent Near Me failure: the test device runs
GrapheneOS with no Google Play Services, so there's no network/fused
location provider — only raw GPS, which can't fix indoors. This was
never a code bug.
- Add 'no_provider' GeoErrorKind; detect it from native error text
(disabled / provider / settings / unavailable).
- Show accurate, actionable messaging: tell the user to enable Network
Location (Settings → Location) — the GrapheneOS-friendly fix — or use
GPS outdoors, instead of a vague timeout.
- Refine the timeout copy along the same lines.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Append the underlying native/browser error message to the failure toast
(and log it) with a 12s duration, so the exact failure reason is visible
on-device without needing adb. This lets us distinguish OS Location-off,
provider-unavailable, timeout, and plugin errors at a glance.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The real reason Near Me failed indoors: getCurrentPosition with high
accuracy waits for a single satisfactory GPS reading, which needs
satellite visibility and times out indoors. Google Maps feels instant
because it takes the FIRST update from any provider (wifi/cell/fused).
Rewrite the acquisition strategy to do the same:
- raceForFirstFix() starts a low-accuracy watchPosition and resolves on
the very first update (the fast network/fused fix, ~1s indoors),
racing it against a cheap cached getCurrentPosition. First to return
wins; watch is cleared immediately after.
- 25s deadline falls back to one long high-accuracy attempt for the
cold-outdoor-GPS case.
- Apply the same watchPosition-race to the web path.
- watchPosition/clearWatch added to the plugin proxy interface, guarded
so builds without them fall back to getCurrentPosition.
- Update timeout toast (drop the "walk to a window" advice; point at the
GPS/Location quick-setting toggle instead).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Solves the "spots that change appearance or disappear" problem with
crowd-sourced, trust-weighted status reports — since the spot owner
often isn't the one who notices a spot got knobbed/demolished/busted.
New custom kind 2259 (regular, append-only): any user can report a
spot's current physical state — active / bust / skate-stopped / closed /
gone — with an optional note, referencing the spot via its `a`
coordinate.
Client aggregation (useSpotStatus / useBatchSpotStatus):
- recency-weighted (30-day half-life) so fresh reports dominate and a
rebuilt spot naturally recovers
- trust-weighted via existing useBatchTrustScores (0.2–1.0 multiplier)
- most-recent-report-per-reporter (no ballot stuffing)
- reports older than 180 days ignored
UI:
- SpotStatusBadge (colored consensus badge + tooltip w/ report count)
- ReportSpotStatusDialog (status picker, optional note, recent reports)
- Badge + Report Status button on SpotDetailPage
- Batched status badge on SpotCard; "gone" spots dimmed/grayscaled
- "gone" spots hidden from map markers (still listed dimmed) to keep the
map uncluttered while preserving the record
Documented the kind, tags, status values, and aggregation rules in
NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Now that the native crash is fixed (Kotlin 2.1 alignment on the Android
side), the remaining issue was GPS "Location timed out" on a cold first
fix / indoors. Rework the native position acquisition into three
increasingly-forgiving stages that return the first success:
1. INSTANT — accept a cached/coarse fix up to 5 min old (resolves
immediately on a warm chip).
2. PRECISE — high-accuracy GPS with a generous 30s window so a cold
first-fix has room to complete.
3. COARSE FALLBACK — network/wifi fix with the longest window to
rescue the "GPS can't lock indoors" case.
Also bump the web-path timeout to 20s, and make the "Location timed out"
toast actionable on native (suggests stepping near a window / retrying).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Bug: after taking a Quick Snap photo and choosing "keep", the user never
reached the Add Spot form. handleQuickCameraCapture uploaded the photo to
Blossom FIRST (which needs an Amber signature and can be slow/fail) and only
navigated on success — so a stalled upload stranded the user right after the
camera.
Fix: Quick Snap now prioritizes speed —
- MapPage stashes the raw photo as a data URL (quickCapturePendingPhoto),
grabs location (non-blocking), and navigates to /create-spot immediately.
- CreateSpotPage reads the pending photo on mount, converts it back to a File,
and uploads it in the background with an "Uploading photo…" tile, adding the
resulting URL to the images list. A failed upload now shows a toast on the
form instead of blocking navigation. Old quickCaptureImage (already-uploaded
URL) path kept for backward-compat.
- Removed the now-unused useUploadFile from MapPage.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The native Geolocation fix only takes effect once the APK is rebuilt — the
current phone build still uses navigator.geolocation, hence the error. To make
the rebuilt version more robust:
- If a high-accuracy fix fails/times out (common indoors or weak GPS), retry
with a coarse (network/wifi) fix and a longer window before giving up. This
rescues most "Location unavailable" cases.
- On native, the "unavailable" toast now tells the user to turn on Location/GPS
AND allow the app permission, instead of the generic web message.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Near Me "location blocked" on the APK:
- navigator.geolocation returns PERMISSION_DENIED inside the Android WebView
because the WebView never gets the OS location permission, so the app
showed a bogus "your browser blocked location" help sheet.
- New src/lib/geolocation.ts: platform-aware getCurrentPosition(). On native
it uses Capacitor's Geolocation plugin via registerPlugin('Geolocation')
from bundled @capacitor/core (same runtime pattern as the camera/Amber
fixes) and calls requestPermissions() to trigger the real Android prompt;
on web it falls back to navigator.geolocation.
- MapPage handleNearMe, proximity sort, and quick-snap location now use it.
On native, a denied permission shows an OS-permission toast instead of the
browser help sheet.
- Added @capacitor/geolocation dependency (native side picked up via cap sync;
no bare import in the JS bundle).
2. Desktop map didn't fit the viewport (had to scroll to see footer):
- Map wrapper was md:h-[calc(100vh-4rem)] — full height minus header only,
leaving no room for the footer.
- Added --footer-desktop token (5rem) and changed the desktop height to
calc(100vh-4rem-var(--footer-desktop)) so map + footer fit without scroll.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New useDeleteEntity hook: publishes a kind:5 deletion request referencing
the target addressable event by `a` coord, `e` id, and `k` kind. Owner-only.
- New reusable DeleteEntityDialog: owner-gated trigger + confirmation with a
clear "deletion is best-effort on decentralized relays" warning, redirect,
and query invalidation so the item drops out of lists.
- Wired "Disband" into CrewDetailPage (founder only, redirects to /crews).
- Wired a "Manage → Delete Profile" card into CoachDetailPage for all Lab
profile types (coach/filmer/editor/photographer/builder), redirects to /lab.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: quickCapture.ts relied on window.Capacitor.Plugins.Camera,
which is only populated once something imports '@capacitor/camera'. Since
we deliberately never import that package (to avoid the old white-screen
crash), the plugin was never registered, the native check failed, and Quick
Snap fell through to the web file input — opening the gallery.
Fix: use Capacitor's official registerPlugin('Camera') from the bundled
@capacitor/core. This returns a proxy that dispatches to the native Camera
implementation over the bridge without needing the JS @capacitor/camera
package to self-register, and without putting a bare '@capacitor/camera'
specifier in the bundle. Applied the same pattern to CameraCapture.tsx.
Also: on a permission error, show a clear "enable camera access" toast
instead of silently opening the gallery.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The previous file-input-only approach fixed the crash but opened the gallery
on native because capture="environment" is only a hint the OS can ignore.
New platform-aware flow (lib/quickCapture.ts):
- Native (APK): open the OS camera directly via the runtime-injected
window.Capacitor.Plugins.Camera with source:'CAMERA' — forces the real
camera and gives built-in preview/retake. Accessed off the runtime object,
so no '@capacitor/camera' specifier in the bundle (keeps the white-screen
fix intact). User cancel is treated as a no-op.
- Web: falls back to the file input with capture="environment".
- If the native camera errors, gracefully falls back to the file input.
MapPage Quick Snap now routes through handleQuickSnap() → the captured photo
feeds the existing upload → create-spot flow unchanged. Bump 1.0.69.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Phase 0 — identity (lib/crewAgent.ts):
- Deterministic per-crew agent keypair derived from the crew coordinate
(sha256 of a public salt + coord). Documented security tradeoff: the key
is reproducible by design and holds no funds — used only for the agent to
speak for itself. Includes kind-0 profile builder (bot:true, NIP-24) and
a signAsCrewAgent helper for Phase 3.
Phase 1 — opt-in toggle:
- SkateCrew.agentEnabled parsed from a queryable ["t","crew-agent"] marker.
- "Crew Agent" toggle (Beta) in CrewForm; publishes the marker when on.
- NIP.md documents the crew-agent tag and the agent identity derivation.
Phase 2 — crew-scoped Sasquatch with receipts:
- lib/sasquatchBrain.ts: shared AI caller (model fallback) + tool-call parser.
- useCrewAgentChat: reuses the CVM data tools, seeded with crew context
(name/location) so "near us" resolves to the crew's turf. Ephemeral/local,
no keys, no publishing yet.
- CrewAgentPanel: chat dialog rendering agent answers PLUS structured receipt
cards (spots / events / Lab pros / gear / crews) linking into the app.
- CrewDetailPage: "Ask Sasquatch" card + panel, shown only when the crew
enabled the agent.
Also: Lab category tabs → fixed 5-across grid (no horizontal scroll). Bump 1.0.68.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The custom in-app CameraCapture failed on the native APK ("Could not open
the camera") due to Capacitor camera permission plumbing, and a single
photo is all a spot snap needs. Replace the Quick Snap flow on the map with
a hidden native file input (accept="image/*" capture="environment"): one
tap opens the OS camera/gallery, the chosen photo feeds the existing upload
→ create-spot flow unchanged. Works identically on web and native, no
plugin/permission fragility. Removes the CameraCapture usage + import from
MapPage (component still used by CreateSpotPage).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The Lab category tabs (Coaches/Filmers/Photo/Editors/Builders) overflowed
the viewport and scrolled sideways on phones, hiding Builders. Replace the
overflow-x row with a 5-column grid of compact icon-over-label tabs so all
five are visible at once without lateral scrolling, with the count badge
floated to the corner so it never widens a tab.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Add visually-hidden (sr-only) titles to the three rendered dialogs/sheets
that were missing them:
- OnboardingPage relay-setup dialog (RelayEducation)
- Layout mobile navigation sheet
- MapPage spot detail bottom sheet
These satisfy Radix's accessibility requirement (screen readers announce
the dialog purpose) without altering the visual design.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewChat: founder+admins can hide chat messages via a per-message hide button; reuses useCrewModeration/useHideCrewPost with the rumor (kind-14) message id
- Hidden messages filtered from chat for everyone; moderators see them greyed + "Hidden" (transparent/reversible)
- Since NIP-17 messages are E2EE and can't be deleted from relays, moderation is enforced client-side via public NIP-32 labels (label reveals only the hidden id, never content)
- Wire admins/crewCoord/canModerate from CrewDetailPage; header hint for moderators
- NIP.md: expand moderation section to cover both feed (kind-1 note id) and chat (kind-14 rumor id) with the E2EE client-side caveat
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useCrewModeration: fetch NIP-32 `hidden` labels authored by founder+admins for a crew; useHideCrewPost publishes a kind-1985 hidden label (L/l org.skatespots.moderation, e=post, a=crew)
- CrewDetailPage feed: moderators can hide posts via an EyeOff button + confirm dialog; hidden posts filtered from the feed for everyone, shown greyed/"Hidden from feed" to moderators (reversible/auditable)
- CrewInviteDialog: founder+admins share an invite link/message (native share + copy); founder gets a shortcut to add a member key in crew settings
- Gate all admin actions on useCrewRole().canManage; moderators = founder + delegated admins
- Add MODERATION_LABEL_NAMESPACE/HIDDEN constants
- NIP.md: document feed moderation label format (author must be founder/admin) + invite semantics
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- SkateCrew: add nip05 + admins fields; parse `admin` role marker on p tags (admins = subset of members)
- CrewForm: NIP-05 input + per-member "Make admin" toggle; write nip05 tag + admin-marked p tags (works on create & edit via shared form)
- useCrewNip05: verify crew handle on ANY domain against the founder pubkey (verified/mismatch/unreachable), CORS-proxy fallback + caching
- CrewNip05Badge: green ✓ / red ⚠ / muted handle states with tooltips
- CollectiveMembers: show Admin chips, sort admins first, admin count in summary
- useCrewRole: centralized founder/admin/member/none roles (canManage, canEditProfile) — founder owns identity, admins get delegated powers with their own keys; egalitarian shared-editing intentionally unsupported
- CrewDetailPage: NIP-05 badge by crew name, viewer role badges (admin/member)
- NIP.md: document crew nip05 verification, admin role marker, and governance/conflict-resolution model
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Lab:
- useLabProfiles sorts featured (Pro) profiles first
- ProBadge shown on Lab cards (LabPage) + Lab detail page; featured cards
get a subtle amber highlight
Events (same no-paywall model as Lab):
- New FeaturedEventUpgrade: every event posts for free; optional fee adds a
featured badge + top placement (reuses existing featuredEvent fee)
- CreateEventPage: removed the paid-event fee gate; opt-in featured toggle,
writes a `featured` tag when active
- useSkateEvents sorts featured events first; EventCard shows ProBadge
(compact in list view, overlay on the image in full view)
Docs:
- NIP.md documents the `featured` tag (unix-seconds expiry, self-attested)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Basic Lab profiles are now always free to publish. The 5,000 sat fee is
opt-in and unlocks a Pro badge + featured placement instead of gating
publication behind a rate.
- types.ts: `featuredUntil` field on all 5 Lab types + SkateEvent,
parseFeaturedUntil() + isFeaturedActive() helpers, wired into parsers
- usePlatformFee: exposes featuredUntilSeconds for the `featured` tag
(platform owners get a 1yr window)
- New ProBadge + LabProUpgrade (opt-in toggle → reveals PlatformFeePayment)
- All 5 Become pages: removed rate-based fee gate and wallet-required gate;
publish is free; when Pro is active a `["featured", "<expiry>"]` tag is added
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Built on the existing NIP-17 gift-wrapped group DM system (useGroupConversation
+ useSendDirectMessage), so the relay only ever sees encrypted blobs.
- New CrewChat component: message list w/ author profiles, composer
(Enter to send), auto-scroll, skeleton/empty states
- Participants = founder + all listed members (deduped)
- Only crew members can read/write (others see a locked "ask to join" state);
warns if the signer lacks NIP-44
- "Private" lock badge makes the E2E-encrypted nature unmistakable
- Surfaced as a card on the crew detail page under the Crew Feed
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useGrowthStats now queries kind 35004 (photographers) and 35005 (builders),
which were previously omitted; added to totals, monthly buckets, allContent,
and the content-mix donut (folded into the "Lab" slice)
- Community Stats page reorganized into clear sub-sections:
* "The Lab · Creators & Services" — Coaches, Filmers, Editors,
Photographers, Builders, with a total badge
* "Community" — Tours, Crews
This scales the growing role list cleanly instead of cramming one row.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Spot Chat:
- Compact TrustFilter dropdown in the chat header (All / Verified / Trusted / Following)
- Filters messages by author trust via useBatchTrustScores; own messages
always stay visible; shows a "N hidden by your trust filter" indicator
Community stats:
- useGrowthStats now queries kind 35006 crews (was missing entirely)
- totalCrews added to totals + monthly buckets + content-mix donut
- New "Crews" StatCard on the Community Stats page
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Inspired by BitChat's location-channel concept, reframed for skaters and
kept fully SkateSpots-native (own kind + tag conventions, no interop).
- New ephemeral kind 24899 tagged with a geohash `g` (documented in NIP.md)
- useSpotChat / useSendSpotChat hooks (polled, community-relay aware)
- SpotChat component: live message list, composer, author profiles,
Block/Neighborhood/City precision toggle, and light IRC-style flavor
(/me action messages, /who participant count)
- Spot detail pages now show a per-spot block-level channel
- Map gains a "Local Chat" sheet anchored to the map center with the
precision selector
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- useCrewZap hook: splits a total evenly across recipients, resolves each
member's Lightning address (batched kind-0 query), fetches an invoice per
recipient and auto-pays via NWC → WebLN; per-recipient status
(resolving/paying/success/skipped/failed) with manual-invoice fallback
- splitAmount util distributes remainder fairly (no lost sats)
- ZapCrewDialog: amount presets + custom, per-head preview, live progress rows,
copy-invoice fallback when no wallet; excludes the current user
- CrewDetailPage: "Zap the Crew" button (recipients = founder + confirmed
members via useCollectiveMembers); shown only when >1 recipient
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Nutzap:
- Gate sending behind NUTZAP_SENDING_ENABLED=false with clear docs; sending
would require P2PK-locking proofs (fund-loss risk if wrong), so it's disabled
until implementable/auditable. Receiving + reading unchanged. No dead-end
button exists (hook was never wired to UI).
Cross-linking crews ↔ Lab collectives:
- types: parseAddrCoord/parseCrewRef/parseLabRefs helpers + LAB_KINDS; add
crewRef to all 5 Lab interfaces/parsers and labRefs[] to SkateCrew
- CrewLinkField (pick one of your own crews) added to all 5 Become-* forms,
shown only for collectives; publishes an a-tag on the Lab profile
- LabLinkField (pick your own Lab profiles) added to CrewForm; publishes a-tags
- CrossLinkCards: LinkedCrewCard on Lab detail ("Part of a Crew"), LinkedLabCard
on crew detail ("Hire This Crew"), resolving coordinates via useCrew/useLabProfile
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove LGBTQ+ and Longboard preset vibes
- Rename "Girls / Womxn" → "Girl / Woman"
- Add custom vibe/focus field: users define their own #hashtag focus
(normalized to a lowercase hyphenated slug, published as t tags)
- Support multiple "Other Socials" links (SkateCrew.socials[]) with add/remove UI;
parser collects all social tags; detail page renders each (URL or plain handle)
- Update NIP.md crew vibe taxonomy
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Phase 1 CLINK integration (recognize & display only):
- types.ts: optional noffer field + isValidNoffer guard on SkateCoach,
SkateFilmer, SkateEditor, SkatePhotographer, SkateShop; parsers read it
- NofferField: reusable opt-in input with live validation, added to all 4
Become-* Lab forms and both Create/Edit shop forms
- ClinkPayButton: secondary "Pay via CLINK" action (QR + copy + nostr: deep
link), rendered on CoachDetailPage and ShopDetailPage only when a valid
noffer is present; never replaces the existing zap / Lightning Address flow
- useQRCodeScanner: classify scanned noffer1/ndebit1 CLINK codes
- NIP.md: document the noffer tag on Lab kinds + shops and a CLINK section
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New Photographers category (blue-indigo) — separate from Filmers:
- New addressable kind 35004 + SkatePhotographer type + parseSkatePhotographer.
- Rich photographer taxonomy as queryable `t` tags: services (action,
sequence, editorial, product, events…), camera bodies, lenses, lighting
(Profoto/Elinchrom/Godox/triggers…), support/rig, and shooting style,
plus a freeform "gear" tag and a portfolio link.
- BecomePhotographerPage form with chip-grid selectors for all gear
categories + "what's in the bag" textarea, wired to /become-photographer.
- The Lab: 4th "Photo" tab (indigo accent) with its own filters, cards,
empty state; OfferFAB gains "Offer Photography"; useLabProfiles fetches
35004; CoachDetailPage renders photographers (and editors) properly with
per-type accent, gear + portfolio display.
- NIP.md: documented the whole Lab (kinds 35001-35004) incl. photographer.
Filmer improvements (now video-focused, photography split out):
- Removed the token "Action Photography" service; added follow/line filming.
- Richer equipment list: cinema camera, HVX, action cam, gimbal, 4K/HFR.
- Filmers gain a freeform "what's in the bag" gear field + a reel/portfolio
link, both surfaced on the detail page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Events can now be discovered directly on the map:
- New "Events" layer (lime #ccff00, calendar pin marker) plotting upcoming,
geo-located events. Off by default — users opt in via the Layers control.
- Event markers show a popup with title, date/time, location, and a
"View Event" button linking to the event detail page.
- Only future/ongoing events with a valid geohash are plotted (past events
are filtered out to keep the map clean).
- Added to the mobile Layers popover, desktop Layers popover, and the mobile
filter drawer's "Show on Map" grid. Layer count badge now tracks 5 layers.
- SpotMap gains `events` + `showEvents` props and a createEventIcon marker.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
#6 Desktop parity:
- Replaced the four separate Spots/Shops/Gear/Tours toggle buttons in the
desktop map toolbar with a single grouped "Layers" popover (with count
badge), matching the mobile control. Declutters the dense toolbar row.
#7 Entity color discipline:
- Locked the four map-entity colors to a single canonical value each,
matching the map markers (source of truth): Spots #ff0040, Shops #9333ea,
Gear #f97316, Tours #06b6d4.
- Fixed the mismatch I'd introduced (Layers popover + Add FAB used #a855f7
purple instead of the marker's #9333ea). Everything now agrees.
Note: broader purple usage (Stash, Cashu, NWC, Editors) is intentionally
left as-is — those are separate semantic uses, not the map entity language.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Map search input: replaced non-standard `bg-white/6` with `bg-white/5`
(matching The Lab's inputs) and removed the focus ring offset so typed
text is now visible on the dark toolbar instead of a white field. Also
normalized the mobile filter button's `bg-white/6` -> `bg-white/5`.
- Bumped Wallet and Settings page titles to text-4xl md:text-5xl to match
The Lab and Events headers for consistent title rhythm.
- Trimmed the map filter drawer's Quick Actions to Marketplace + Stash,
since Add Spot / Shop / Tour / Quick Snap now live in the Add speed-dial
FAB (removes duplication).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replaces the crowded 12-item vertical right rail with a clean, minimal set:
- Right edge now has just two 44px controls: Near Me and a Layers button.
- Layers opens a labelled popover (Spots/Shops/Gear/Tours) with color dots
and checkmarks, plus a count badge on the button so state is obvious.
- All "add" actions consolidated into a single red speed-dial FAB
(bottom-right) that fans out to Add Spot / Add Shop / Offer Tour /
Quick Snap, with a tap-catcher backdrop to dismiss.
- Sasquatch bubble moved to bottom-left on mobile so it no longer competes
with the Add FAB for the bottom-right corner.
- Larger touch targets (44px) for better native/ZapStore ergonomics.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Sasquatch panel no longer auto-opens on the map (/) or The Lab (/lab).
Added an `autoShow` flag to GuideMessage; only opt-in messages (onboarding)
auto-open. The bubble stays minimized until tapped, and context messages
still populate the panel when opened manually.
- Map CARTO/OSM attribution now sits above the mobile bottom nav (safe-area
aware) instead of overlapping the Shops & Spots buttons; made it smaller
and more subtle. Desktop keeps it in the bottom-left corner.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Architecture:
- usePlatformFee hook: central fee config (featuredEvent/labProfile/featuredShop)
5,000 sats each, 30-day validity, localStorage persistence, owner bypass
- PlatformFeePayment component: generic reusable payment gate
auto-pay via NWC/WebLN, QR fallback, active/expired state display
all fees route to skatespots@minibits.cash
Events (CreateEventPage):
- New optional "Entry Fee" field
- Triggers 5,000 sat platform fee when filled
- Stores entry-fee + paid tags on published event
- Free events: zero friction unchanged
The Lab (Become{Coach,Filmer,Editor}Page):
- Triggers 5,000 sat fee when "Rate" field is filled
- Submit disabled with clear CTA until fee paid
- Free/volunteer profiles: zero friction unchanged
Rule: "pay only when you earn"
- No rate set → publish instantly, no fee
- Rate set → 5,000 sats to SkateSpots, valid 30 days
- Platform owners always bypass
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Hero:
- "SKATE YOUR WORLD" headline with neon red/lime treatment
- Beta badge pill with live pulse dot
- Feature pills row (spots, lab, events, gear, stash, radio, zaps)
- Primary CTA right in the hero — no scrolling to get started
- "No email. No password. Own your identity forever." copy
Layout:
- Removed mechanical 4-step progress tracker (step 2 auto-completing was confusing)
- Replaced with natural scrolling sections with clear headings
- Logged-in state shows green confirmation bar with FounderFollow
What you can do:
- 6-card grid: Map, Lab, Events, Buy/Sell, Stash, Radio — all linked
What is Nostr:
- Skater-friendly explanation with 4 benefit pills
- Deep dive button for relay education modal
Zaps & Wallet:
- "Optional" badge so new users don't feel overwhelmed
- Side-by-side: Cashu (built-in) + External wallet (NWC)
FAQ (accordion — 8 questions, no duplicates):
- "Is it free?" — combined the two identical free questions
- "What is Nostr?" — proper explanation
- "What's an nsec?" — key backup warning
- "What signers work?" — desktop/Android/iOS with real links
- "Can I use it on my phone?" — PWA + Zapstore mention
- "What is a zap?" — simple skater framing
- "Will my spots be deleted?" — relay permanence
- "Private property spots?" — community responsibility
- "How to report a bug?" — links to cordn.net beta coordinator
Beta section:
- cordn.net/p/npub1e2ayjl... as primary beta contact (clickable card)
- Founder npub with Copy + DM buttons as secondary
- Removed v0.1.0 hardcoded version string
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Both + buttons are 36px circles matching the strip visual system
- Add Spot = red circle (bottom of layer toggles section)
- Add Shop = green circle (right below Add Spot)
- Removed redundant left-side FAB entirely — everything is now in one column on the right
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Toolbar:
- Slim rounded-pill search input (was tall boxy default)
- Compact icon-only view toggle pills
- Small circle filter button (red when active, badge count)
Right-side control strip:
- Layer toggles (Spots/Shops/Gear/Tours) as 36px circles
active=opaque, inactive=35% opacity — no more giant emoji FABs
- Near Me button integrated into strip (removed from top-right)
- Quick Snap in strip, same style — no more big red circle
- All transitions: translate-x-2 slide-out when overlays open
Left-side Add FAB:
- Single gradient pill (red→orange) replaces two competing circles
- Smaller footprint, less visual noise
Loading indicator:
- Replaced center-screen "Loading..." pill with slim animated
gradient bar at top edge of map (top-0 left-0 right-0 h-0.5)
Attribution:
- Moved from floating top-right to bottom-left corner
- 9px text, 50% opacity — required but invisible
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Sidebar expanded to 300px with real visual weight
- Inventory card: big stat number (e.g. "5 items in stash"), est. sats value, 3-cell secondary stats row (Wishlist / For Sale / To Trade), clickable category breakdown bars
- Marketplace CTA: gradient header with branded icon, per-type badges showing sell/trade counts, full-width gradient CTA button
- Wishlist teaser card: number callout + "View Wishlist" action button
- Quick-add buttons (Add to Stash / Add to Wishlist) moved to top of sidebar on desktop — header no longer needs them
- Grid defaults to 2/3/4 cols (sm/md/xl) — no more awkward single-item stretch
- Empty states bigger and more purposeful with better copy
- Mobile layout unchanged; FABs and mobile CTA banner retained
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- REMOVED: Sync banner (already in header subtitle)
- REMOVED: ProfileStatsBar self-intro card (it's YOUR page, you know who you are)
- REMOVED: Chunky tab switcher — replaced with underline tabs inside the sticky header,
saving ~50px of vertical space before you see any items
- MERGED: All header info (title + item count + encryption + sats value) into one
compact 56px sticky bar — nothing duplicated
- IMPROVED: Search + category filter in a single tight row (h-9 inputs)
- IMPROVED: Category chips only render when >1 category present (no orphan chips)
- IMPROVED: Sidebar is desktop-only (lg:hidden) — on mobile it was buried below items,
now replaced by a contextual bottom banner that only appears when items are for sale
- IMPROVED: FABs slightly smaller (not competing with Sasquatch bubble)
- IMPROVED: Empty state is more focused — no walls of text
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Merged the two separate marketplace cards (Ready to Sell + Stash→Marketplace)
into a single smart card:
- When items are marked for sale/trade: shows orange "Ready to Sell" CTA
with item count and a prominent Browse Marketplace button
- When no items for sale: shows a quieter grey card explaining how to
mark items for sale, with a subdued Browse Marketplace link
One card, one button, no duplication.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Key improvements:
- NEW: List view (default) — compact swipeable rows for mobile, perfect
for scanning inventory quickly. Grid view still available via toggle.
- NEW: Floating FABs on mobile — red + to add to stash, cyan heart to
add to wishlist, positioned above the nav bar with safe-area support
- NEW: Cross-device sync info banner explaining Nostr = synced everywhere
(answers the user's question: yes, items added on desktop show on mobile)
- FIX: "Browse Marketplace" now navigates to /marketplace (GearSwapPage)
instead of /map?view=gear which incorrectly loaded the map
- NEW: Sticky page header with item count badge, encrypt indicator, and
view-mode toggle — always visible when scrolling
- NEW: Compact ProfileStatsBar replaces the heavy CharacterPanel sidebar card
- NEW: Category quick-chips — tap an emoji to filter by category instantly
- IMPROVED: Location filter only shows locations that have items (no empty chips)
- IMPROVED: Sidebar consolidated — removed redundant cards, kept what matters:
"Ready to Sell" card (only shown if items are marked for sale), marketplace
link, wishlist status, and compact tips
- IMPROVED: Empty states mention cross-device sync as a feature
- IMPROVED: List row actions are always visible (no hover-only on mobile)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- FABs (add spot/shop): switch from fixed bottom-[88px] to
calc(3.5rem + env(safe-area-inset-bottom, 0px) + 0.75rem) so they
respect device safe-area and always clear the bottom nav on all
Android/iOS Capacitor builds
- Right-side filter pills: same fix, switched from absolute to fixed
with the same safe-area-aware bottom value
- useSasquatchGuide: /map page now returns undefined message, meaning
Sasquatch never auto-opens when navigating to Shops & Spots — user
must tap the floating bubble to open it manually
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Root cause: map container is already clipped to exclude bottom nav
(h-[calc(100dvh-var(--header-mobile)-var(--bottom-nav))]) so
bottom-[calc(5rem+...)] was pushing buttons ~80px up inside the
clipped box — wrong and covered filter pills
- Both FABs and right-side pills now use bottom-4 (16px from container edge)
- FABs redesigned as clean 48×48 icon-only circles, no text labels:
red circle (+) → Add Spot
green circle (+) → Add Shop
thick Plus strokeWidth=3, high-contrast (black on green, white on red)
- Only shown when user is logged in (no logged-out ghost button)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove speed-dial/tap-to-expand pattern — buttons are now always visible
- "Add Spot" — red (#ff0040), white text, same style as before
- "Add Shop" — green (#00cc66), black text, sits above Add Spot
- Both cleared of bottom nav via bottom-[calc(5rem+env(safe-area-inset-bottom))]
- Removed unused fabOpen state
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
FAB (Add Spot / Add Shop):
- Removed old bottom-left "Add Spot" pill that sat on top of the bottom nav
- New speed-dial FAB in bottom-left, positioned at
bottom-[calc(5rem+env(safe-area-inset-bottom,0px))] — always clears
the iOS/Android safe area + bottom nav height
- Tap the red + button to expand upward: "Add Spot" (red/MapPin) and
"Add Shop" (purple/Store) pill buttons slide in with animation
- + button rotates 45° when open (becomes ×), closes on map tap
- Logged-out state shows "Log in to add" prompt instead
- Tapping either option closes the speed-dial before navigating
Right-side filter pills:
- Bumped from bottom-20 to bottom-[calc(5rem+env(safe-area-inset-bottom))]
so they're consistently safe-area aware on devices with home indicator
Near Me — location denied fix:
- PERMISSION_DENIED no longer shows a toast (which was useless — user
can't act on a disappearing toast)
- Tapping the red LocateOff button now opens a bottom-sheet help dialog
with per-platform instructions (Android Chrome, iOS Safari, desktop
Chrome, Firefox) on how to re-enable location permission
- "Got it" dismisses and resets the error state so the button returns
to normal for a fresh attempt
- handleNearMe still fires normally on first tap (not denied state)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
QR rendering:
- Fixed layout shift (CLS) — canvas and spinner now occupy the same fixed
236×236 container; spinner fades out, canvas fades in, no height jump
- Added error state with Retry button when QRCode.toCanvas() throws
- Canvas now has role="img" + descriptive aria-label for screen readers
- "Save QR" button exports canvas → PNG via toDataURL (mobile share flow)
Deep link / "Open in app":
- Replaced document.createElement('a').click() with window.location.href
(the only reliable way to fire custom schemes on iOS Safari PWA)
- Added 2 s fallback timeout: if the page doesn't lose focus (app not
installed), automatically redirects to App Store / GitHub releases
- Button shows "Opening…" state while the timeout is pending
- Added hint text explaining the fallback behaviour
Platform detection:
- detectPlatform() reads navigator.userAgent to identify iOS/Android/desktop
- Download list is sorted so the most-relevant entry appears first
- Each download row now has a sublabel (e.g. "iPhone & iPad", "arm64 devices")
- Added Android APK entry (was missing)
Accessibility:
- All interactive buttons have focus-visible:ring-2 keyboard focus rings
- Decorative icons marked aria-hidden="true" throughout
- "How it works" list changed from <div> to <ol>/<li> with aria-label
- Download list uses role="list" / role="listitem"
- Use-cases grid uses <ul>/<li> with aria-label
- "Learn more" link has full descriptive aria-label inc. "(opens in new tab)"
- Action buttons have aria-label for screen readers
Contrast & copy:
- URI preview: text-muted-foreground/60 → text-white/70 (passes WCAG AA)
- URI box is now select-all / cursor-text for easy manual copy
- Hero icon changed from Network → Shield (semantically: encrypted/private)
- Step 2 text adapts when logged out ("Log in to generate your QR")
- Use-case emoji changed 🎬→🔒 for "Private crew comms" (more on-message)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New "Crew Network" menu item in Settings > App, positioned below Relays
- Full CrewNetworkScreen sub-screen with:
- Hero explainer card (nostr-vpn private mesh VPN concept)
- Live QR code generated via `qrcode` canvas for the join-request URI
(nvpn://join-request?pubkey=<hex>&name=<displayName>&relay=...)
- Copy link button + "Open in app" deep link button
- Step-by-step "How It Works" list
- Download links for iOS App Store, TestFlight beta, and GitHub releases
- 2x2 use-case grid (private crew comms, secret spots, censorship bypass, offline mesh)
- GitHub "Learn more" row
- Logged-out state shows a LoginArea prompt
- TypeScript Section type extended with 'crewnetwork'
- Cleaned up unused Palette/Settings lucide imports
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- SasquatchGuide now dispatches 'sasquatch:panelchange' event when panel opens/closes
- MapPage listens to the event and hides the Add Spot FAB (with scale-down animation)
whenever the Sasquatch panel is open — no more overlap
- FAB reappears with smooth opacity + scale transition when panel closes
- Added a visible close (X) button to the Sasquatch panel header alongside the existing
minimize button — gives users a clear way to fully dismiss the panel
- Sasquatch panel bottom edge stays 10px above the nav bar on all screen sizes
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
CRITICAL:
- Near Me button: completely rewritten — was silently discarding ALL errors. Now shows
toast for denied, timeout, unavailable with actionable copy. Button shows red LocateOff
icon when denied so user sees the state at a glance. Raised to 44×44px (Apple minimum).
- Map height on mobile: switched to CSS variable system (--header-mobile, --bottom-nav)
for pixel-accurate height. Added Layout noPadding prop so the map page gets full bleed
without the 80px bottom-padding fighting the map container calc.
- Layer toggle pills: raised from 28×28 to 40×40px — was below touch target minimum
- Right-side control column: now hides when spot bottom sheet is open (was always visible)
iOS/Directions:
- SpotDetailPage directions button: now detects Apple platform and opens Apple Maps on
iOS/macOS, Google Maps everywhere else (was always Google Maps)
- Added aria-label to directions button
Accessibility:
- LoginDialog: was missing DialogTitle, causing console errors on every login open
- CameraCapture: fallback dialog was missing DialogTitle (sr-only)
- CommandDialog: added sr-only DialogTitle
Layout:
- SpotDetailPage: reduced top padding mobile pt-4 (was py-8 = 32px wasted space)
- Directions popover: centered in viewport (top-1/2 left-1/2) with zoom-in animation,
never clips behind Sheet stacking context
CSS:
- Added --header-mobile and --bottom-nav CSS variables for consistent calculations
- Added .page-fullbleed helper
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Fix iOS directions: Apple Maps first on Apple platforms, removed broken geo: URI,
added Waze option, fixed popover z-index clipping behind Sheet component
- Fix 'Me' bottom nav: now deep-links to /settings?section=profile for logged-in users
so tapping 'Me' shows your profile, not a generic settings menu
- Add SettingsPage URL param support: ?section=X opens sub-screen directly
- Add onboarding welcome banner: dismissible strip at top of map for logged-out users
with 'Get Started' CTA pointing to /onboarding, stored in localStorage
- Fix Lab empty state: FAQ link + login prompt for logged-out users
- Fix Events empty state: FAQ link added to all empty state instances
- Fix GearSwap empty state: actionable 'Clear Filters' button + FAQ link
- Community stats: add 'this month' delta badges (+N this month) on all stat cards
- Newsletter pill: hide on map page to eliminate bottom sheet overlap conflict
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>