SkateSpots/src/pages/WalletPage.tsx
shakespeare.diy bb6b3e6b7e Fix map search input visibility; align header sizes; de-dupe map Quick Actions
- Map search input: replaced non-standard `bg-white/6` with `bg-white/5`
  (matching The Lab's inputs) and removed the focus ring offset so typed
  text is now visible on the dark toolbar instead of a white field. Also
  normalized the mobile filter button's `bg-white/6` -> `bg-white/5`.
- Bumped Wallet and Settings page titles to text-4xl md:text-5xl to match
  The Lab and Events headers for consistent title rhythm.
- Trimmed the map filter drawer's Quick Actions to Marketplace + Stash,
  since Add Spot / Shop / Tour / Quick Snap now live in the Add speed-dial
  FAB (removes duplication).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 10:24:52 +07:00

271 lines
10 KiB
TypeScript

import { useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout';
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 { cn } from '@/lib/utils';
import {
Zap,
Settings,
CheckCircle,
XCircle,
Globe,
WalletMinimal,
Nut,
ChevronRight,
ExternalLink,
ChevronDown,
ChevronUp,
Wallet,
} 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-600'
)}>
<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 (
<Layout>
<div className="container px-4 py-5 max-w-lg mx-auto">
{/* Header */}
<div className="mb-4">
<h1 className="text-4xl md:text-5xl font-black uppercase leading-none mb-1">
<span className="neon-orange">WALLET</span>
</h1>
<p className="text-muted-foreground text-sm">Bitcoin Lightning + Cashu</p>
</div>
{user ? (
<div className="space-y-4">
{/* Status bar + manage — compact single row */}
<WalletStatusBar />
{/* Cashu wallet — the one and only balance + send/receive UI */}
<CashuWalletCard />
{/* Get a wallet — collapsed by default */}
<GetAWallet />
{/* What are zaps — auto-collapses once wallet connected */}
<WhatAreZaps walletConnected={walletConnected} />
</div>
) : (
<NotLoggedIn />
)}
</div>
</Layout>
);
}