318 lines
14 KiB
TypeScript
318 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 { useTranslation } from '@/contexts/language';
|
||
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 = [
|
||
{
|
||
titleKey: 'walletUi.appShop.sticker',
|
||
size: '55 × 86 mm',
|
||
tipKey: 'walletUi.appShop.stickerTip',
|
||
file: '/images/print/skatespots-sticker.svg',
|
||
},
|
||
{
|
||
titleKey: 'walletUi.appShop.poster',
|
||
size: 'A3 · 297 × 420 mm',
|
||
tipKey: 'walletUi.appShop.posterTip',
|
||
file: '/images/print/skatespots-poster.svg',
|
||
},
|
||
];
|
||
|
||
/** What we're working on. Not products, not prices — just intent. */
|
||
const PLANNED = [
|
||
{ icon: Shirt, labelKey: 'walletUi.appShop.tees' },
|
||
{ icon: Package, labelKey: 'walletUi.appShop.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, labelKey: 'walletUi.appShop.shopTools' },
|
||
];
|
||
|
||
const PAY_CHIPS = [
|
||
{ icon: Zap, labelKey: 'walletUi.appShop.lightning' },
|
||
{ icon: Coins, labelKey: 'walletUi.appShop.cashu' },
|
||
{ icon: MapPin, labelKey: 'walletUi.appShop.localHandover' },
|
||
];
|
||
|
||
export default function AppShopPage() {
|
||
const { t } = useTranslation();
|
||
useSeoMeta({
|
||
title: t('seoUi.appShopTitle'),
|
||
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={t('walletUi.appShop.title')}
|
||
accent="#ccff00"
|
||
subtitle={t('walletUi.appShop.subtitle')}
|
||
/>
|
||
|
||
{/* ── 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" />
|
||
{t('walletUi.appShop.comingSoon')}
|
||
</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">
|
||
{t('walletUi.appShop.wearThe')}
|
||
</span>
|
||
<br />
|
||
<span className="text-white">{t('walletUi.appShop.scene')}</span>
|
||
</h2>
|
||
|
||
<p className="mt-4 max-w-xl text-sm leading-relaxed text-gray-300 sm:text-base">
|
||
{t('walletUi.appShop.heroDesc1')}{' '}
|
||
<strong className="font-bold text-white">{t('walletUi.appShop.locally')}</strong>{t('walletUi.appShop.heroDesc2')}{' '}
|
||
<strong className="font-bold text-[#f7931a]">sats</strong>{t('walletUi.appShop.heroDesc3')}
|
||
</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" />
|
||
{t('walletUi.appShop.requestItem')}
|
||
</Link>
|
||
</Button>
|
||
<span className="text-[11px] text-gray-500">
|
||
{t('walletUi.appShop.dmHint')}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="mt-5 flex flex-wrap gap-2">
|
||
{PAY_CHIPS.map(({ icon: Icon, labelKey }) => (
|
||
<span
|
||
key={labelKey}
|
||
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]" />
|
||
{t(labelKey)}
|
||
</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">
|
||
{t('walletUi.appShop.inTheWorks')}
|
||
</h3>
|
||
<div className="h-px flex-1 bg-white/10" />
|
||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||
{t('walletUi.appShop.notForSale')}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
{PLANNED.slice(0, 2).map(({ icon: Icon, labelKey }) => (
|
||
<div
|
||
key={labelKey}
|
||
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">
|
||
{t(labelKey)}
|
||
</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">
|
||
{t('walletUi.appShop.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">{t('walletUi.appShop.stickerPacks')}</p>
|
||
<p className="mt-0.5 text-xs leading-relaxed text-gray-400">
|
||
{t('walletUi.appShop.stickerPacksDesc')}
|
||
</p>
|
||
</div>
|
||
<span className="shrink-0 rounded-full bg-[#ccff00] px-2 py-0.5 text-[9px] font-black uppercase tracking-widest text-black">
|
||
{t('common.free')}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
{FREE_PRINT.map((item) => (
|
||
<div
|
||
key={item.titleKey}
|
||
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={t('walletUi.appShop.printableAlt', { title: t(item.titleKey) })}
|
||
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">{t(item.titleKey)}</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" />
|
||
{t(item.tipKey)}
|
||
</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" /> {t('walletUi.appShop.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" /> {t('common.save')}
|
||
</a>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<p className="mt-3 text-[11px] leading-relaxed text-gray-600">
|
||
{t('walletUi.appShop.printNote1')} <strong className="text-gray-400">{t('walletUi.appShop.actualSize')}</strong> {t('walletUi.appShop.printNote2')}
|
||
</p>
|
||
</div>
|
||
|
||
{PLANNED.slice(2).map(({ icon: Icon, labelKey }) => (
|
||
<div
|
||
key={labelKey}
|
||
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">
|
||
{t(labelKey)}
|
||
</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">
|
||
{t('walletUi.appShop.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">
|
||
{t('walletUi.appShop.howBuying')}
|
||
</h3>
|
||
<div className="h-px flex-1 bg-white/10" />
|
||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||
{t('walletUi.appShop.dmNotCart')}
|
||
</span>
|
||
</div>
|
||
|
||
<ol className="grid gap-3 sm:grid-cols-3">
|
||
{[
|
||
{
|
||
n: '1',
|
||
tk: 'walletUi.appShop.step1Title',
|
||
dk: 'walletUi.appShop.step1Desc',
|
||
},
|
||
{
|
||
n: '2',
|
||
tk: 'walletUi.appShop.step2Title',
|
||
dk: 'walletUi.appShop.step2Desc',
|
||
},
|
||
{
|
||
n: '3',
|
||
tk: 'walletUi.appShop.step3Title',
|
||
dk: 'walletUi.appShop.step3Desc',
|
||
},
|
||
].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">{t(s.tk)}</p>
|
||
<p className="mt-1 text-xs leading-relaxed text-gray-400">{t(s.dk)}</p>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
</section>
|
||
|
||
</div>
|
||
);
|
||
}
|