feat(app-shop): add the App Shop section (empty on purpose)
New /app-shop page for SkateSpots merch, to be paid in sats only (Lightning or
Cashu) — no cards, no accounts, no middleman.
Design: gradient/glow hero ('Wear the scene' + Coming soon badge), an honest
stock strip (0 items / None pre-orders / Nothing for sale today), an 'In the
works' teaser grid (tees, decks, stickers, tools — dashed + 'soon', no prices),
a three-step 'How it'll work' (pick -> pay in sats -> ships), a newsletter
notify band (the drop lands there first) and a plain-English honesty footer.
Deliberately contains no items: no stock, no prices, no pre-orders, no buy
buttons. AGENTS.md documents that rule so nobody invents a catalogue later.
Wiring: registered in NAV_CATALOG as id 'app-shop' (appears in the desktop
'More' menu AND the mobile burger menu automatically), lazy route in AppRouter,
and added to the smoke-test route list so a broken page aborts deploys.
Verified: page renders desktop + mobile (all sections), listed in both navs,
smoke PASS with /app-shop.
This commit is contained in:
parent
c07f857ed0
commit
8dcc97fd81
6 changed files with 256 additions and 0 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -344,6 +344,17 @@ session with **no other memory of this project**. So:
|
|||
burger menu (`MOBILE_MENU_LISTED_HREFS`) now **appends catalog items that
|
||||
aren't hard-coded**, so Marketplace/Mints (and any future section) are always
|
||||
reachable on mobile — don't re-hardcode only a subset.
|
||||
- **App Shop** (`/app-shop`, `src/pages/AppShopPage.tsx`): SkateSpots merch, to be
|
||||
paid in **sats only (Lightning or Cashu)** — no cards/accounts. Registered in
|
||||
`NAV_CATALOG` as id `app-shop` (label "App Shop", `ShoppingBag`), which is what
|
||||
makes it appear in **both** the desktop "More" menu and the mobile burger menu
|
||||
(the mobile menu appends catalog items not in `MOBILE_MENU_LISTED_HREFS`). It is
|
||||
**intentionally empty**: no stock, no prices, no pre-orders and nothing for
|
||||
sale — the copy states that plainly ("an empty shop with good lighting") and
|
||||
only previews what's coming. **Do not add prices/fake urgency/buy buttons until
|
||||
there is real stock**; when items exist, model them as listings and take
|
||||
Lightning/Cashu payment. The page reads the live stock strip as 0/None/Nothing
|
||||
— keep it honest if that changes.
|
||||
- **Mints directory**: `/mints` page lists Cashu mints from the community
|
||||
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
|
||||
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
v1.5.4
|
||||
|
||||
🛍 App Shop — a new section (empty on purpose)
|
||||
- Merch is coming, and it'll be paid in sats: Lightning or Cashu, your choice.
|
||||
No cards, no accounts, no processor skimming the drop.
|
||||
- To be clear: we hold no stock and nothing is for sale yet. No pre-orders, no
|
||||
countdown timers, no fake scarcity — the page says so, and previews what's in
|
||||
the works. Subscribe to the newsletter to get the drop first.
|
||||
|
||||
✉️ Direct messages actually arrive now
|
||||
- Your DM inbox relays were being forgotten every time the app loaded, so
|
||||
messages were sent to your normal relays instead of the ones the other person
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ const ROUTES = [
|
|||
'/collections',
|
||||
'/stats',
|
||||
'/mints',
|
||||
'/app-shop',
|
||||
'/messages/group/abc123',
|
||||
];
|
||||
const problems = [];
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const EventsPage = lazyWithRetry(() => import("./pages/EventsPage"));
|
|||
const LabPage = lazyWithRetry(() => import("./pages/LabPage"));
|
||||
const WalletPage = lazyWithRetry(() => import("./pages/WalletPage"));
|
||||
const MintsPage = lazyWithRetry(() => import("./pages/MintsPage"));
|
||||
const AppShopPage = lazyWithRetry(() => import("./pages/AppShopPage"));
|
||||
const SettingsPage = lazyWithRetry(() => import("./pages/SettingsPage"));
|
||||
const CreateSpotPage = lazyWithRetry(() => import("./pages/CreateSpotPage"));
|
||||
const CreateShopPage = lazyWithRetry(() => import("./pages/CreateShopPage"));
|
||||
|
|
@ -134,6 +135,7 @@ export function AppRouter() {
|
|||
<Route path="/food" element={<FoodPage />} />
|
||||
<Route path="/wallet" element={<WalletPage />} />
|
||||
<Route path="/mints" element={<MintsPage />} />
|
||||
<Route path="/app-shop" element={<AppShopPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/create-spot" element={<CreateSpotPage />} />
|
||||
<Route path="/create-shop" element={<CreateShopPage />} />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
BarChart3,
|
||||
Coins,
|
||||
Handshake,
|
||||
ShoppingBag,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
|
|
@ -60,6 +61,7 @@ export const NAV_CATALOG: NavTab[] = [
|
|||
{ id: 'crews', href: '/crews', labelKey: 'nav.crews', label: 'Crews', icon: Users },
|
||||
{ id: 'wallet', href: '/wallet', labelKey: 'nav.wallet', label: 'Wallet', icon: Wallet, authOnly: true },
|
||||
{ id: 'mints', href: '/mints', label: 'Mints', icon: Coins },
|
||||
{ id: 'app-shop', href: '/app-shop', label: 'App Shop', icon: ShoppingBag },
|
||||
{ id: 'trades', href: '/marketplace', label: 'Marketplace', icon: Handshake },
|
||||
{ id: 'stash', href: '/stash', label: 'Stash', icon: Package, authOnly: true },
|
||||
{ id: 'collections', href: '/collections', label: 'Collections', icon: Bookmark, authOnly: true },
|
||||
|
|
|
|||
233
src/pages/AppShopPage.tsx
Normal file
233
src/pages/AppShopPage.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ShoppingBag,
|
||||
Zap,
|
||||
Coins,
|
||||
Truck,
|
||||
Shirt,
|
||||
Sticker,
|
||||
Package,
|
||||
Hammer,
|
||||
Sparkles,
|
||||
BadgeCheck,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { NewsletterSubscribeButton } from '@/components/NewsletterSubscribeButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/**
|
||||
* App Shop — SkateSpots merch.
|
||||
*
|
||||
* Deliberately a STOREFRONT WITH NOTHING IN IT: there is no stock, no
|
||||
* pre-order, no price and nothing to buy. It exists so the section, the design
|
||||
* language and the payment story (sats only — Lightning or Cashu) are in place
|
||||
* before the first drop. When items land we can model them as listings and take
|
||||
* Lightning/Cashu payment; until then this page must never imply a sale.
|
||||
*/
|
||||
|
||||
/** What we're working on. Not products, not prices — just intent. */
|
||||
const PLANNED = [
|
||||
{ icon: Shirt, label: 'Tees & hoodies', note: 'Heavyweight, printed somewhere that pays people properly.' },
|
||||
{ icon: Package, label: 'Decks', note: 'Small numbered runs. Ride it or hang it, your call.' },
|
||||
{ icon: Sticker, label: 'Sticker packs', note: 'For the box, the rail, the laptop lid.' },
|
||||
{ icon: Hammer, label: 'Shop tools', note: 'The hardware the homies actually wear out.' },
|
||||
];
|
||||
|
||||
const PAY_CHIPS = [
|
||||
{ icon: Zap, label: 'Lightning' },
|
||||
{ icon: Coins, label: 'Cashu' },
|
||||
{ icon: Truck, label: 'Shipped worldwide' },
|
||||
{ icon: BadgeCheck, label: 'At cost, no markup theatre' },
|
||||
];
|
||||
|
||||
export default function AppShopPage() {
|
||||
useSeoMeta({
|
||||
title: 'App Shop — SkateSpots',
|
||||
description:
|
||||
'SkateSpots merch, paid in sats over Lightning or Cashu. Nothing in stock yet — here is what is coming.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl px-4 py-8 pb-24 md:pb-14">
|
||||
<PageHeader
|
||||
icon={ShoppingBag}
|
||||
title="App Shop"
|
||||
accent="#ccff00"
|
||||
subtitle="SkateSpots merch for the people who keep the scene rolling."
|
||||
/>
|
||||
|
||||
{/* ── Hero ─────────────────────────────────────────────────────────── */}
|
||||
<section className="relative overflow-hidden rounded-3xl border-2 border-[#ff0040]/25 bg-gradient-to-br from-[#ff0040]/12 via-black to-[#ccff00]/[0.06] px-5 py-10 sm:px-8 sm:py-14">
|
||||
<div className="pointer-events-none absolute -top-28 left-1/2 h-64 w-64 -translate-x-1/2 rounded-full bg-[#ff0040]/25 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-24 -right-16 h-56 w-56 rounded-full bg-[#ccff00]/15 blur-3xl" />
|
||||
|
||||
<div className="relative">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-[#ccff00]/40 bg-[#ccff00]/10 px-3 py-1 text-[10px] font-black uppercase tracking-widest text-[#ccff00]">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Coming soon
|
||||
</span>
|
||||
|
||||
<h2
|
||||
className="mt-4 text-5xl font-black uppercase leading-[0.9] sm:text-7xl"
|
||||
style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}
|
||||
>
|
||||
<span className="bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00] bg-clip-text text-transparent">
|
||||
Wear the
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-white">Scene</span>
|
||||
</h2>
|
||||
|
||||
<p className="mt-4 max-w-xl text-sm leading-relaxed text-gray-300 sm:text-base">
|
||||
Merch for the people who keep this thing rolling. Paid in{' '}
|
||||
<strong className="font-bold text-[#f7931a]">sats</strong> — Lightning or Cashu,
|
||||
whichever you already keep. No cards, no accounts, no middleman skimming the drop.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{PAY_CHIPS.map(({ icon: Icon, label }) => (
|
||||
<span
|
||||
key={label}
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-white/12 bg-white/[0.04] px-3 py-1.5 text-[11px] font-bold text-gray-300 backdrop-blur-sm"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 text-[#ccff00]" />
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Honest stock strip ───────────────────────────────────────────── */}
|
||||
<div className="mt-4 grid grid-cols-3 gap-2 sm:gap-3">
|
||||
{[
|
||||
{ k: 'Items in stock', v: '0' },
|
||||
{ k: 'Pre-orders', v: 'None' },
|
||||
{ k: 'For sale today', v: 'Nothing' },
|
||||
].map((s) => (
|
||||
<div
|
||||
key={s.k}
|
||||
className="rounded-2xl border border-white/8 bg-white/[0.03] px-3 py-4 text-center"
|
||||
>
|
||||
<p className="text-xl font-black text-white sm:text-2xl">{s.v}</p>
|
||||
<p className="mt-1 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
{s.k}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── What's in the works ──────────────────────────────────────────── */}
|
||||
<section className="mt-10">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<h3 className="text-sm font-black uppercase tracking-widest text-white">
|
||||
In the works
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-white/10" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
not for sale yet
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{PLANNED.map(({ icon: Icon, label, note }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="group relative overflow-hidden rounded-2xl border-2 border-dashed border-white/12 bg-white/[0.02] p-4 transition-colors hover:border-[#ccff00]/30"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#ccff00]/10 text-[#ccff00]">
|
||||
<Icon className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="font-black uppercase tracking-wide text-white">{label}</p>
|
||||
<p className="mt-0.5 text-xs leading-relaxed text-gray-400">{note}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="absolute right-3 top-3 rounded-full border border-white/12 px-2 py-0.5 text-[9px] font-black uppercase tracking-widest text-gray-500">
|
||||
soon
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── How it'll work ───────────────────────────────────────────────── */}
|
||||
<section className="mt-10">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<h3 className="text-sm font-black uppercase tracking-widest text-white">
|
||||
How it'll work
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-white/10" />
|
||||
</div>
|
||||
|
||||
<ol className="grid gap-3 sm:grid-cols-3">
|
||||
{[
|
||||
{ n: '1', t: 'Pick your thing', d: 'One page, no logged-in pricing games, no fake urgency.' },
|
||||
{ n: '2', t: 'Pay in sats', d: 'Lightning or Cashu. Straight to us — no processor in the middle.' },
|
||||
{ n: '3', t: 'It ships', d: 'Posted to your door with an honest tracking number.' },
|
||||
].map((s) => (
|
||||
<li
|
||||
key={s.n}
|
||||
className="rounded-2xl border border-white/8 bg-white/[0.03] p-4"
|
||||
>
|
||||
<span
|
||||
className="mb-3 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-[#ff0040] to-[#ff6600] text-sm font-black text-white"
|
||||
style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<p className="font-black uppercase tracking-wide text-white">{s.t}</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-gray-400">{s.d}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* ── Notify band ──────────────────────────────────────────────────── */}
|
||||
<section className="mt-10 rounded-3xl border-2 border-[#ccff00]/20 bg-[#ccff00]/[0.04] px-5 py-6 text-center sm:px-8">
|
||||
<h3 className="text-lg font-black uppercase tracking-wide text-white">
|
||||
Want the drop first?
|
||||
</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-xs leading-relaxed text-gray-400">
|
||||
The newsletter is where merch lands first — sizes, colours, and the real
|
||||
price in sats. Subscribe and it reaches your notification bell.
|
||||
</p>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<NewsletterSubscribeButton />
|
||||
</div>
|
||||
<p className="mt-4 text-[11px] text-gray-600">
|
||||
Got a design you want on a deck?{' '}
|
||||
<Link to="/newsletter" className="text-[#ccff00] hover:opacity-80">
|
||||
Tell us in the community section
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ── Honesty foot ─────────────────────────────────────────────────── */}
|
||||
<section className="mt-8 rounded-2xl border border-white/8 bg-white/[0.02] p-4">
|
||||
<p className="text-xs leading-relaxed text-gray-500">
|
||||
<strong className="text-gray-300">The honest bit:</strong> we hold no stock and
|
||||
nothing here is for sale. There are no pre-orders, no countdown timers and no
|
||||
fake scarcity — when there is something real to buy, this page will say so and
|
||||
take your sats. Until then it is just an empty shop with good lighting.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline" className="gap-2 border-white/15">
|
||||
<Link to="/supporters">
|
||||
Back the project <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="gap-2 text-gray-400 hover:text-white">
|
||||
<Link to="/mints">
|
||||
<Coins className="h-3.5 w-3.5" /> Pick a mint for your sats
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue