The sticker + poster were their own 'Free to print' section, which made the shop read as two lists. They now live inside the 'Sticker packs' card in the 'In the works' grid (full-width, FREE badge, compact previews + Print/Save), between Decks and Shop tools — so a category shows what it already offers. Verified: no 'Free to print' heading remains, both printables still render, and the page reads hero -> in the works (5 entries) -> how buying works.
321 lines
14 KiB
TypeScript
321 lines
14 KiB
TypeScript
import { useSeoMeta } from '@unhead/react';
|
||
import { Link } from 'react-router-dom';
|
||
import { nip19 } from 'nostr-tools';
|
||
import {
|
||
ShoppingBag,
|
||
Zap,
|
||
Coins,
|
||
Shirt,
|
||
Sticker,
|
||
Package,
|
||
Hammer,
|
||
Sparkles,
|
||
MapPin,
|
||
MessageCircle,
|
||
Printer,
|
||
Download,
|
||
Scissors,
|
||
} from 'lucide-react';
|
||
import { PageHeader } from '@/components/PageHeader';
|
||
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.
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
/**
|
||
* Free, printable artwork. Physical size is baked into the files (mm units), so
|
||
* "Actual size" printing gives an exact 55 × 86 mm sticker / A3 poster, and the
|
||
* QR in each one resolves to https://skate-spots.org.
|
||
*/
|
||
const FREE_PRINT = [
|
||
{
|
||
title: 'Sticker',
|
||
size: '55 × 86 mm',
|
||
tip: 'Black die-cut. Print on sticker paper, cut along the black edge.',
|
||
file: '/images/print/skatespots-sticker.svg',
|
||
},
|
||
{
|
||
title: 'Poster',
|
||
size: 'A3 · 297 × 420 mm',
|
||
tip: 'Prints on A3; shrink to A4 (50%) and it still scans fine.',
|
||
file: '/images/print/skatespots-poster.svg',
|
||
},
|
||
];
|
||
|
||
/** What we're working on. Not products, not prices — just intent. */
|
||
const PLANNED = [
|
||
{ icon: Shirt, label: 'Tees & hoodies' },
|
||
{ icon: Package, label: 'Decks' },
|
||
// 'Sticker packs' is rendered separately in the grid, because it is the one
|
||
// entry that already contains something real: the free printables.
|
||
{ icon: Hammer, label: 'Shop tools' },
|
||
];
|
||
|
||
const PAY_CHIPS = [
|
||
{ icon: Zap, label: 'Lightning' },
|
||
{ icon: Coins, label: 'Cashu' },
|
||
{ icon: MapPin, label: 'Local handover' },
|
||
];
|
||
|
||
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 — sold{' '}
|
||
<strong className="font-bold text-white">locally, on request</strong>. You message
|
||
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>
|
||
|
||
<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 }) => (
|
||
<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>
|
||
|
||
{/* ── 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.slice(0, 2).map(({ icon: Icon, label }) => (
|
||
<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>
|
||
<p className="min-w-0 self-center font-black uppercase tracking-wide text-white">
|
||
{label}
|
||
</p>
|
||
</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>
|
||
))}
|
||
|
||
{/* Sticker packs — the only thing you can actually GET today: the free
|
||
printables. Kept inside its category (not a separate section) so the
|
||
shop reads as one list of merch, free stuff included. */}
|
||
<div className="rounded-2xl border-2 border-dashed border-[#ccff00]/35 bg-[#ccff00]/[0.04] p-4 sm:col-span-2">
|
||
<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]">
|
||
<Sticker className="h-5 w-5" />
|
||
</span>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="font-black uppercase tracking-wide text-white">Sticker packs</p>
|
||
<p className="mt-0.5 text-xs leading-relaxed text-gray-400">
|
||
The packs are coming. The designs are not — print these two right now, free.
|
||
</p>
|
||
</div>
|
||
<span className="shrink-0 rounded-full bg-[#ccff00] px-2 py-0.5 text-[9px] font-black uppercase tracking-widest text-black">
|
||
Free
|
||
</span>
|
||
</div>
|
||
|
||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||
{FREE_PRINT.map((item) => (
|
||
<div
|
||
key={item.title}
|
||
className="overflow-hidden rounded-xl border border-white/10 bg-black/40"
|
||
>
|
||
<div className="flex h-36 items-center justify-center p-3">
|
||
<img
|
||
src={item.file}
|
||
alt={`SkateSpots ${item.title} — printable`}
|
||
className="max-h-full max-w-full drop-shadow-[0_4px_14px_rgba(0,0,0,0.6)]"
|
||
/>
|
||
</div>
|
||
<div className="border-t border-white/8 p-3">
|
||
<div className="flex items-baseline justify-between gap-2">
|
||
<p className="font-bold uppercase tracking-wide text-white">{item.title}</p>
|
||
<span className="shrink-0 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||
{item.size}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 flex items-start gap-1.5 text-[11px] leading-relaxed text-gray-500">
|
||
<Scissors className="mt-0.5 h-3 w-3 shrink-0 text-gray-600" />
|
||
{item.tip}
|
||
</p>
|
||
<div className="mt-2.5 flex flex-wrap gap-2">
|
||
<Button
|
||
asChild
|
||
size="sm"
|
||
className="gap-1.5 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90"
|
||
>
|
||
<a href={item.file} target="_blank" rel="noopener noreferrer">
|
||
<Printer className="h-3.5 w-3.5" /> Print
|
||
</a>
|
||
</Button>
|
||
<Button asChild size="sm" variant="outline" className="gap-1.5 border-white/15">
|
||
<a href={item.file} download>
|
||
<Download className="h-3.5 w-3.5" /> Save
|
||
</a>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<p className="mt-3 text-[11px] leading-relaxed text-gray-600">
|
||
Print at <strong className="text-gray-400">100% / “Actual size”</strong> so the size is
|
||
exact and the QR stays sharp — it points at skate-spots.org. Vector files, so they
|
||
scale without going fuzzy.
|
||
</p>
|
||
</div>
|
||
|
||
{PLANNED.slice(2).map(({ icon: Icon, label }) => (
|
||
<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>
|
||
<p className="min-w-0 self-center font-black uppercase tracking-wide text-white">
|
||
{label}
|
||
</p>
|
||
</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 buying works
|
||
</h3>
|
||
<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>
|
||
|
||
<ol className="grid gap-3 sm:grid-cols-3">
|
||
{[
|
||
{
|
||
n: '1',
|
||
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) => (
|
||
<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>
|
||
|
||
</div>
|
||
);
|
||
}
|