feat(app-shop): local/on-request sales via DM (drop shipping claims)

- Remove the 'Shipped worldwide' and 'At cost, no markup theatre' chips; the
  remaining chips are Lightning, Cashu, Local handover.
- Copy now says sold locally, on request: message me -> we sort the details ->
  pay in sats at handover. No shipping, at all.
- 'How it'll work' -> 'How buying works' (it's a DM, not a cart): Message me /
  We sort it out / Pay in sats.
- New 'Request an item' CTA in the hero that deep-links into a private DM with
  the shop owner (/messages/<PLATFORM_OWNER_PUBKEYS[0] npub>), so a buyer can
  start the conversation with zero checkout machinery.
- Honesty footer states local handover only / no shipping of any kind.
- AGENTS.md documents the model (no cart/checkout/shipping without a decision).
This commit is contained in:
SkateSpots 2026-09-27 21:52:00 +07:00
parent 8dcc97fd81
commit 059d9da5c4
2 changed files with 61 additions and 16 deletions

View file

@ -345,7 +345,12 @@ session with **no other memory of this project**. So:
aren't hard-coded**, so Marketplace/Mints (and any future section) are always aren't hard-coded**, so Marketplace/Mints (and any future section) are always
reachable on mobile — don't re-hardcode only a subset. reachable on mobile — don't re-hardcode only a subset.
- **App Shop** (`/app-shop`, `src/pages/AppShopPage.tsx`): SkateSpots merch, to be - **App Shop** (`/app-shop`, `src/pages/AppShopPage.tsx`): SkateSpots merch, to be
paid in **sats only (Lightning or Cashu)** — no cards/accounts. Registered in paid in **sats only (Lightning or Cashu)** — no cards/accounts — sold
**locally, on request**: there is **no cart, no checkout, no shipping**. Buying
is a conversation: "Request an item" deep-links to a **DM with the owner**
(`/messages/<owner npub>`, owner = `PLATFORM_OWNER_PUBKEYS[0]`), the details
are agreed in chat, and sats change hands at handover. Don't add shipping
copy/rates or a cart without a decision to change that model. Registered in
`NAV_CATALOG` as id `app-shop` (label "App Shop", `ShoppingBag`), which is what `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 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 (the mobile menu appends catalog items not in `MOBILE_MENU_LISTED_HREFS`). It is

View file

@ -1,21 +1,27 @@
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { import {
ShoppingBag, ShoppingBag,
Zap, Zap,
Coins, Coins,
Truck,
Shirt, Shirt,
Sticker, Sticker,
Package, Package,
Hammer, Hammer,
Sparkles, Sparkles,
BadgeCheck, MapPin,
MessageCircle,
ArrowRight, ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import { PageHeader } from '@/components/PageHeader'; import { PageHeader } from '@/components/PageHeader';
import { NewsletterSubscribeButton } from '@/components/NewsletterSubscribeButton'; import { NewsletterSubscribeButton } from '@/components/NewsletterSubscribeButton';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
/** Where "request an item" goes: an encrypted DM straight to the shop owner. */
const SHOP_OWNER_NPUB = nip19.npubEncode(PLATFORM_OWNER_PUBKEYS[0]);
const REQUEST_HREF = `/messages/${SHOP_OWNER_NPUB}`;
/** /**
* App Shop — SkateSpots merch. * App Shop — SkateSpots merch.
@ -38,8 +44,7 @@ const PLANNED = [
const PAY_CHIPS = [ const PAY_CHIPS = [
{ icon: Zap, label: 'Lightning' }, { icon: Zap, label: 'Lightning' },
{ icon: Coins, label: 'Cashu' }, { icon: Coins, label: 'Cashu' },
{ icon: Truck, label: 'Shipped worldwide' }, { icon: MapPin, label: 'Local handover' },
{ icon: BadgeCheck, label: 'At cost, no markup theatre' },
]; ];
export default function AppShopPage() { export default function AppShopPage() {
@ -81,12 +86,30 @@ export default function AppShopPage() {
</h2> </h2>
<p className="mt-4 max-w-xl text-sm leading-relaxed text-gray-300 sm:text-base"> <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{' '} Merch for the people who keep this thing rolling — sold{' '}
<strong className="font-bold text-[#f7931a]">sats</strong> — Lightning or Cashu, <strong className="font-bold text-white">locally, on request</strong>. You message
whichever you already keep. No cards, no accounts, no middleman skimming the drop. me, we sort the details, and you pay in{' '}
<strong className="font-bold text-[#f7931a]">sats</strong> — Lightning or Cashu —
when you collect it. No cards, no accounts, no checkout robot, no middleman
skimming the drop.
</p> </p>
<div className="mt-6 flex flex-wrap gap-2"> <div className="mt-6 flex flex-wrap items-center gap-3">
<Button
asChild
className="gap-2 bg-[#ccff00] font-black uppercase text-black hover:bg-[#ccff00]/90"
>
<Link to={REQUEST_HREF}>
<MessageCircle className="h-4 w-4" />
Request an item
</Link>
</Button>
<span className="text-[11px] text-gray-500">
Opens a private, encrypted DM with me.
</span>
</div>
<div className="mt-5 flex flex-wrap gap-2">
{PAY_CHIPS.map(({ icon: Icon, label }) => ( {PAY_CHIPS.map(({ icon: Icon, label }) => (
<span <span
key={label} key={label}
@ -158,16 +181,31 @@ export default function AppShopPage() {
<section className="mt-10"> <section className="mt-10">
<div className="mb-4 flex items-center gap-3"> <div className="mb-4 flex items-center gap-3">
<h3 className="text-sm font-black uppercase tracking-widest text-white"> <h3 className="text-sm font-black uppercase tracking-widest text-white">
How it'll work How buying works
</h3> </h3>
<div className="h-px flex-1 bg-white/10" /> <div className="h-px flex-1 bg-white/10" />
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
it's a DM, not a cart
</span>
</div> </div>
<ol className="grid gap-3 sm:grid-cols-3"> <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: '1',
{ n: '3', t: 'It ships', d: 'Posted to your door with an honest tracking number.' }, t: 'Message me',
d: 'Send a DM with what you want — item, size, colour, whatever you are after.',
},
{
n: '2',
t: 'We sort it out',
d: 'Price in sats, and where and when to meet. It is a conversation, not a checkout.',
},
{
n: '3',
t: 'Pay in sats',
d: 'Lightning or Cashu at handover, then it is yours. No shipping, no waiting rooms.',
},
].map((s) => ( ].map((s) => (
<li <li
key={s.n} key={s.n}
@ -211,9 +249,11 @@ export default function AppShopPage() {
<section className="mt-8 rounded-2xl border border-white/8 bg-white/[0.02] p-4"> <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"> <p className="text-xs leading-relaxed text-gray-500">
<strong className="text-gray-300">The honest bit:</strong> we hold no stock and <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 nothing here is for sale. There are no pre-orders, no countdown timers and no fake
fake scarcity — when there is something real to buy, this page will say so and scarcity — and it is <strong className="text-gray-300">local handover only</strong>,
take your sats. Until then it is just an empty shop with good lighting. arranged by DM, so there is no shipping of any kind. When there is something real,
this page will say so and we will sort the details between us. Until then it is
just an empty shop with good lighting.
</p> </p>
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
<Button asChild variant="outline" className="gap-2 border-white/15"> <Button asChild variant="outline" className="gap-2 border-white/15">