- AppRouter wraps every route (except /amber-callback and the 404) in ONE
<Route element={<Layout/>}> with an <Outlet/>; the chrome now persists across
navigation instead of being torn down and rebuilt on every page.
- Layout: children optional (falls back to <Outlet/>); the map is full-bleed via
a route check (isFullBleed) instead of a per-page noPadding prop.
- All 59 pages + CraftProfileForm/ErrorState/NewsletterIssueView now render
fragments instead of <Layout> (structure-preserving), imports dropped.
Removes the whole remount class: chrome no longer re-runs its hooks per
navigation, Sasquatch chat state survives navigation, and the header can't
re-open Amber on every page change.
- Lab forms: a non-empty email must be valid and website/portfolio must
resolve to an https URL; invalid input shows an inline hint and blocks
publish. Adds isValidEmail() (reused by sanitizeEmail). social/instagram
are exempt since they may be bare @handles.
- New PhotoDropzone component: drag & drop plus multi-select image input,
emitting File[] to the caller. Wired into CreateSpotPage + EditSpotPage
(drop several photos at once) and AddPhotoToSpot, replacing the old
one-file-at-a-time inputs.
- Spot photo uploads run sequentially and dedupe URLs.
- Preserve the geo 'g' tag and paid 'featured' tag when editing any Lab
profile (all 7 craft roles via CraftProfileForm + 5 bespoke pages).
- Render previously collected-but-hidden fields on the Lab profile page:
judge 'events_judged', organizer 'events_hosted', editor hardware/software,
and spot references.
- Include Event Organizer (35013) in Community Stats + counts.
- Cap Title inputs at 100 (was 120) so profiles no longer fail the parser
and silently vanish; MIN_TITLE_LEN 2 -> 1 for 1-char display names.
- Normalize bare-domain URLs (example.com) to https:// before storing instead
of dropping/hiding them (new normalizeUrlInput helper; fixes extra url fields).
- No crew strip for judging/organizing (was wrongly showing Photo & Film);
scrollable Offer Services FAB; live bio counters.
Finishes the Lab audit: the 5 bespoke 'become' pages now prefill from your
existing profile (edit, not blind overwrite) and expose a 'Headline' field used
as the profile title. Also:
- submit button shows Update... when editing; success toast says updated
- removed the dead ExistingLabProfileNotice (the warning it gave is now moot)
Every one of the 12 Lab provider flows (5 bespoke + 7 craft) now edits + titles
consistently. Gate: 101 tests, 0 errors/warnings, boot smoke green.
Lab provider profiles (coach, filmer, editor, photographer, builder) are
addressable events with a fixed per-type `d` identifier, so re-publishing
REPLACES the existing profile. Unlike BecomeSkaterPage — which pre-fills from
the existing profile so re-submitting edits rather than clobbers — these five
forms start blank and reconstruct nothing. A returning provider who opened the
form again and submitted would silently wipe every field they'd previously set
(bio, rate, spots, contact, links) down to whatever they happened to retype.
Rather than fragile per-page reconstruction of the mixed services/skills tag
sets (high regression risk), added a small honest guardrail:
- New ExistingLabProfileNotice component: appears only when the current user
already has a profile of that type (looked up via useLabProfile by their
pubkey + the known identifier). It states plainly that publishing will
REPLACE the current profile and links to view the existing one first.
- Wired into all five Become* pages with the correct identifier/label.
Also confirmed (not a bug): all five navigate to /coach/:pubkey/:identifier
after publish — CoachDetailPage is a generic Lab-profile renderer that resolves
any type by the distinct `d` identifier, so this is correct despite the name.
Investigation of the shared submit handlers found them otherwise consistent
and well-formed; left untouched.
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>
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>
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>
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>
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>