SkateSpots/src/pages/WalletPage.tsx
SkateSpots 087f490acc refactor(layout): hoist Layout into the router so the chrome mounts once
- 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.
2026-09-27 13:41:42 +07:00

283 lines
11 KiB
TypeScript

import { useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { PageHeader } from '@/components/PageHeader';
import { WalletModal } from '@/components/WalletModal';
import { Badge } from '@/components/ui/badge';
import { useWallet } from '@/hooks/useWallet';
import { useNWC } from '@/hooks/useNWCContext';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useCashu } from '@/hooks/wallet/useCashu';
import { LoginArea } from '@/components/auth/LoginArea';
import { CashuWalletCard } from '@/components/wallet/CashuWalletCard';
import { EscrowKeyCard } from '@/components/wallet/EscrowKeyCard';
import { NwcWalletCard } from '@/components/wallet/NwcWalletCard';
import { cn } from '@/lib/utils';
import {
Zap,
Settings,
CheckCircle,
XCircle,
Globe,
WalletMinimal,
Nut,
ChevronRight,
ExternalLink,
ChevronDown,
ChevronUp,
Wallet,
Coins,
} from 'lucide-react';
// ─── Wallet status bar ────────────────────────────────────────────────────────
function WalletStatusBar() {
const { webln } = useWallet();
const { connections } = useNWC();
const { isInitialized: hasCashu } = useCashu();
const hasNWC = connections.length > 0 && connections.some((c) => c.isConnected);
const isReady = webln || hasNWC || hasCashu;
return (
<div className="flex items-center justify-between gap-3">
{/* Status pills */}
<div className="flex items-center gap-1.5">
<StatusPill icon={Nut} label="Cashu" active={hasCashu} color="text-orange-400" activeBg="bg-orange-500/20 border-orange-500/30" />
<StatusPill icon={Globe} label="WebLN" active={!!webln} color="text-blue-400" activeBg="bg-blue-500/20 border-blue-500/30" />
<StatusPill icon={WalletMinimal} label="NWC" active={hasNWC} color="text-purple-400" activeBg="bg-purple-500/20 border-purple-500/30" />
</div>
{/* Manage wallets */}
<WalletModal>
<button className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/8 active:scale-95 transition-all flex-shrink-0">
<Settings className="h-3.5 w-3.5 text-orange-400" />
<span className="text-xs font-bold text-white">Manage</span>
{isReady
? <Badge className="bg-green-500/20 text-green-400 border-green-500/30 text-[9px] px-1 py-0 leading-none">ON</Badge>
: <ChevronRight className="h-3 w-3 text-muted-foreground" />
}
</button>
</WalletModal>
</div>
);
}
function StatusPill({
icon: Icon, label, active, color, activeBg,
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
active: boolean;
color: string;
activeBg: string;
}) {
return (
<div className={cn(
'flex items-center gap-1 px-2 py-1 rounded-full border text-[10px] font-bold transition-all',
active ? `${activeBg} ${color}` : 'bg-white/3 border-white/8 text-gray-400'
)}>
<Icon className="h-2.5 w-2.5" />
{label}
{active
? <CheckCircle className="h-2.5 w-2.5 text-green-500" />
: <XCircle className="h-2.5 w-2.5 text-gray-700" />
}
</div>
);
}
// ─── Get a wallet (collapsible) ───────────────────────────────────────────────
function GetAWallet() {
const [open, setOpen] = useState(false);
const wallets = [
{ name: 'Minibits', desc: 'Mobile Cashu · NWC', url: 'https://www.minibits.cash', color: 'text-orange-400' },
{ name: 'Alby', desc: 'Browser extension · WebLN + NWC', url: 'https://getalby.com', color: 'text-yellow-400' },
{ name: 'Zeus', desc: 'Self-custodial Lightning', url: 'https://zeusln.com', color: 'text-blue-400' },
{ name: 'Coinos', desc: 'Web-based · works everywhere', url: 'https://coinos.io', color: 'text-green-400' },
];
return (
<div className="rounded-2xl border border-white/8 overflow-hidden">
<button
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-3.5 bg-white/3 hover:bg-white/5 transition-all"
>
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-yellow-400" />
<span className="text-sm font-black uppercase tracking-wide text-white">Get a Wallet</span>
</div>
{open
? <ChevronUp className="h-4 w-4 text-muted-foreground" />
: <ChevronDown className="h-4 w-4 text-muted-foreground" />
}
</button>
{open && (
<div className="divide-y divide-white/5">
{wallets.map((w) => (
<a
key={w.name}
href={w.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between px-4 py-3 hover:bg-white/5 active:scale-[0.99] transition-all"
>
<div className="min-w-0">
<p className={cn('font-bold text-sm', w.color)}>{w.name}</p>
<p className="text-xs text-muted-foreground">{w.desc}</p>
</div>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
</a>
))}
<div className="px-4 py-3">
<a
href="https://planb.academy/en/tutorials/wallet"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-orange-400 hover:text-orange-300 transition-colors flex items-center gap-1.5"
>
<ExternalLink className="h-3 w-3" />
Learn more at Plan B Academy
</a>
</div>
</div>
)}
</div>
);
}
// ─── What are zaps (collapsible, hidden once wallet connected) ────────────────
function WhatAreZaps({ walletConnected }: { walletConnected: boolean }) {
const [open, setOpen] = useState(!walletConnected);
if (walletConnected && !open) {
return (
<button
onClick={() => setOpen(true)}
className="text-xs text-muted-foreground/40 hover:text-muted-foreground transition-colors flex items-center gap-1 mx-auto"
>
<ChevronDown className="h-3 w-3" />
What are zaps?
</button>
);
}
return (
<div className="rounded-2xl border border-white/8 overflow-hidden">
<button
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-3.5 bg-white/3 hover:bg-white/5 transition-all"
>
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-amber-500" />
<span className="text-sm font-black uppercase tracking-wide text-white">What are Zaps?</span>
</div>
{open
? <ChevronUp className="h-4 w-4 text-muted-foreground" />
: <ChevronDown className="h-4 w-4 text-muted-foreground" />
}
</button>
{open && (
<div className="px-4 pb-4 pt-3 space-y-3">
<p className="text-sm text-muted-foreground leading-relaxed">
Instant Bitcoin Lightning payments. Send sats to any skater, spot creator, or coach — no middleman, no platform cut.
</p>
<div className="grid grid-cols-2 gap-2">
{[
{ emoji: '⚡', title: 'Instant', desc: 'Settles in seconds' },
{ emoji: '🔐', title: 'Flexible', desc: 'Your wallet, your keys' },
{ emoji: '🥜', title: 'Private', desc: 'Cashu hides who pays' },
{ emoji: '💜', title: 'Direct', desc: 'Value goes to creator' },
].map((item) => (
<div key={item.title} className="flex items-start gap-2 p-2.5 bg-white/3 rounded-xl">
<span className="text-base flex-shrink-0">{item.emoji}</span>
<div>
<p className="font-bold text-xs text-white">{item.title}</p>
<p className="text-[11px] text-muted-foreground">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
// ─── Not logged in ────────────────────────────────────────────────────────────
function NotLoggedIn() {
return (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 flex items-center justify-center mb-4">
<Wallet className="h-8 w-8 text-orange-500/60" />
</div>
<h3 className="text-lg font-black mb-2 text-white">Login Required</h3>
<p className="text-muted-foreground text-sm max-w-xs mb-6 leading-relaxed">
Log in with your Nostr account to connect your Lightning wallet and start zapping.
</p>
<LoginArea className="justify-center" />
</div>
);
}
// ─── Page ─────────────────────────────────────────────────────────────────────
export default function WalletPage() {
useSeoMeta({
title: 'Wallet - SkateSpots',
description: 'Connect your Lightning wallet to send zaps to skaters and spot creators.',
});
const { user } = useCurrentUser();
const { webln } = useWallet();
const { connections } = useNWC();
const { isInitialized: hasCashu } = useCashu();
const hasNWC = connections.length > 0 && connections.some((c) => c.isConnected);
const walletConnected = !!(webln || hasNWC || hasCashu);
return (
<>
<div className="container px-4 py-5 max-w-lg mx-auto">
<PageHeader title="Wallet" accent="#ff6600" subtitle="Bitcoin Lightning + Cashu" />
{user ? (
<div className="space-y-4">
{/* Status bar + manage — compact single row */}
<WalletStatusBar />
{/* Connected Lightning wallet (NIP-47) — balance / receive / send */}
<NwcWalletCard />
{/* Cashu wallet — native app, self-custodial ecash */}
<CashuWalletCard />
<EscrowKeyCard />
<Link
to="/mints"
className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 transition-colors hover:bg-white/[0.06]"
>
<span className="flex items-center gap-2 text-sm font-medium text-white">
<Coins className="h-4 w-4 text-[#ccff00]" /> Browse Cashu mints
</span>
<ChevronRight className="h-4 w-4 text-gray-500" />
</Link>
{/* Get a wallet — collapsed by default */}
<GetAWallet />
{/* What are zaps — auto-collapses once wallet connected */}
<WhatAreZaps walletConnected={walletConnected} />
</div>
) : (
<NotLoggedIn />
)}
</div>
</>
);
}