SkateSpots/src/pages/WalletPage.tsx
shakespeare.diy 340f6fd89f fix(mobile): redesign wallet page UI for mobile + global mobile polish
Wallet page (/wallet):
- Add BalanceHero banner at top showing Cashu balance + wallet status pills
- Cashu/Lightning tabs with colored active states (orange/yellow)
- Remove nested balance display in CashuWalletCard (now shown in hero)
- Compact balance row in CashuWalletCard
- Stacked full-width inputs on mobile (no side-by-side on small screens)
- h-11 touch targets on all inputs and buttons (44px min)
- inputMode="numeric" on number fields (prevents iOS zoom)
- Receive/Send tabs with green/red active colors for clear context
- History items now 52px min height with colored amount text
- WalletComparisonGuide stays collapsed by default
- Fix: add Nut icon import to CashuWalletCard (was crashing)
- All modals use w-[calc(100%-1rem)] for proper mobile sizing

Settings page (/settings):
- 7-tab bar now scrollable horizontally on mobile (no overflow/wrapping)
- -mx-3 bleed trick so scrollable area goes edge-to-edge
- Wallet tab buttons full-width on mobile
- Container max-w-3xl for better desktop centering

FAQ page (/faq):
- Compact hero on mobile (smaller icon, tighter padding)
- py-6 on mobile instead of py-12

CSS utilities:
- Add xs: breakpoint (375px) variants: flex, flex-row, px-4, -mx-4, gap-2, text-xs/sm, hidden

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-23 16:35:49 +07:00

386 lines
17 KiB
TypeScript

import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout';
import { WalletModal } from '@/components/WalletModal';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWallet } from '@/hooks/useWallet';
import { useNWC } from '@/hooks/useNWCContext';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useCashu } from '@/hooks/wallet/useCashu';
import { useSatsToUsd } from '@/hooks/useExchangeRate';
import { LoginArea } from '@/components/auth/LoginArea';
import { CashuWalletCard } from '@/components/wallet/CashuWalletCard';
import { LightningPrivacyTips } from '@/components/wallet/LightningPrivacyTips';
import { WalletComparisonGuide } from '@/components/wallet/WalletComparisonGuide';
import {
Wallet,
Zap,
Settings,
CheckCircle,
XCircle,
ExternalLink,
Globe,
WalletMinimal,
Info,
Nut,
ChevronRight,
Bitcoin,
} from 'lucide-react';
// ─── Wallet Status Card ───────────────────────────────────────────────────────
function WalletStatusCard() {
const { webln } = useWallet();
const { connections } = useNWC();
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
const cashuUsd = useSatsToUsd(cashuBalance);
const hasNWC = connections.length > 0 && connections.some(c => c.isConnected);
const isReady = webln || hasNWC || hasCashu;
return (
<Card className={isReady ? 'border-green-500/30 bg-green-950/10' : 'border-gray-700'}>
<CardHeader className="pb-3 px-4 pt-4">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-base">
<Wallet className="h-5 w-5 text-primary flex-shrink-0" />
Wallet Status
</CardTitle>
<Badge
variant={isReady ? 'default' : 'secondary'}
className={isReady ? 'bg-green-500/20 text-green-400 border-green-500/30 text-xs' : 'text-xs'}
>
{isReady ? '✓ Ready' : 'Not Connected'}
</Badge>
</div>
<CardDescription className="text-xs mt-1">
{isReady
? 'Your wallet is connected and ready to send zaps!'
: 'Connect a wallet to enable Lightning zaps'}
</CardDescription>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{/* Cashu */}
<div className="flex items-center justify-between p-3 bg-muted/40 rounded-xl min-h-[56px]">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-lg bg-orange-500/15 flex items-center justify-center flex-shrink-0">
<Nut className="h-4 w-4 text-orange-500" />
</div>
<div className="min-w-0">
<p className="font-semibold text-sm">Cashu Wallet</p>
<p className="text-xs text-muted-foreground truncate">
{hasCashu && cashuBalance > 0
? `${cashuBalance.toLocaleString()} sats${cashuUsd ? ` · $${cashuUsd.toFixed(2)}` : ''}`
: 'Built-in ecash wallet'}
</p>
</div>
</div>
{hasCashu
? <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
: <XCircle className="h-5 w-5 text-muted-foreground/50 flex-shrink-0" />}
</div>
{/* WebLN */}
<div className="flex items-center justify-between p-3 bg-muted/40 rounded-xl min-h-[56px]">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-lg bg-blue-500/15 flex items-center justify-center flex-shrink-0">
<Globe className="h-4 w-4 text-blue-400" />
</div>
<div className="min-w-0">
<p className="font-semibold text-sm">WebLN</p>
<p className="text-xs text-muted-foreground">Browser extension wallet</p>
</div>
</div>
{webln
? <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
: <XCircle className="h-5 w-5 text-muted-foreground/50 flex-shrink-0" />}
</div>
{/* NWC */}
<div className="flex items-center justify-between p-3 bg-muted/40 rounded-xl min-h-[56px]">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-lg bg-purple-500/15 flex items-center justify-center flex-shrink-0">
<WalletMinimal className="h-4 w-4 text-purple-400" />
</div>
<div className="min-w-0">
<p className="font-semibold text-sm">Nostr Wallet Connect</p>
<p className="text-xs text-muted-foreground">
{connections.length > 0
? `${connections.length} wallet${connections.length !== 1 ? 's' : ''} connected`
: 'Remote wallet connection'}
</p>
</div>
</div>
{hasNWC
? <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
: <XCircle className="h-5 w-5 text-muted-foreground/50 flex-shrink-0" />}
</div>
<WalletModal>
<Button className="w-full gap-2 mt-1 h-12 rounded-xl font-bold">
<Settings className="h-4 w-4" />
Manage Wallets
</Button>
</WalletModal>
</CardContent>
</Card>
);
}
// ─── Zaps Info Card ───────────────────────────────────────────────────────────
function ZapsInfoCard() {
const items = [
{ emoji: '⚡', title: 'Instant', desc: 'Payments settle in seconds over Lightning' },
{ emoji: '🔐', title: 'Flexible custody', desc: 'Use your own wallet or a custodial service' },
{ emoji: '🥜', title: 'NutZaps for privacy', desc: "Cashu ecash — mints can't see who pays whom" },
{ emoji: '💜', title: 'Value for value', desc: 'Support creators directly' },
];
return (
<Card className="border-gray-700">
<CardHeader className="px-4 pt-4 pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Zap className="h-5 w-5 text-amber-500" />
What are Zaps?
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
<p className="text-sm text-muted-foreground leading-relaxed">
Zaps are Lightning micropayments — send sats instantly to any skater, spot creator, or coach. No middleman, no platform cut.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 pt-1">
{items.map((item) => (
<div key={item.title} className="flex items-start gap-3 p-3 bg-muted/40 rounded-xl">
<span className="text-xl flex-shrink-0 mt-0.5">{item.emoji}</span>
<div>
<p className="font-semibold text-sm">{item.title}</p>
<p className="text-xs text-muted-foreground leading-snug">{item.desc}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
// ─── Recommended Wallets ──────────────────────────────────────────────────────
function WalletResourcesCard() {
const wallets = [
{ name: 'Alby', desc: 'Browser extension · WebLN + NWC', url: 'https://getalby.com', type: 'WebLN', color: 'text-yellow-400' },
{ name: 'Minibits', desc: 'Mobile Cashu wallet · NWC', url: 'https://www.minibits.cash', type: 'NWC', color: 'text-orange-400' },
{ name: 'Zeus', desc: 'Self-custodial mobile Lightning', url: 'https://zeusln.com', type: 'NWC', color: 'text-blue-400' },
{ name: 'Coinos', desc: 'Web-based · works everywhere', url: 'https://coinos.io', type: 'NWC', color: 'text-green-400' },
];
return (
<Card className="border-gray-700">
<CardHeader className="px-4 pt-4 pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Info className="h-5 w-5" />
Recommended Wallets
</CardTitle>
<CardDescription className="text-xs">Get started with one of these Lightning wallets</CardDescription>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{wallets.map((w) => (
<a
key={w.name}
href={w.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-3 bg-muted/40 rounded-xl hover:bg-muted active:scale-[0.98] transition-all group min-h-[56px]"
>
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-lg bg-white/5 flex items-center justify-center flex-shrink-0">
<Wallet className={`h-4 w-4 ${w.color}`} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-semibold text-sm group-hover:text-primary transition-colors">{w.name}</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4">{w.type}</Badge>
</div>
<p className="text-xs text-muted-foreground truncate">{w.desc}</p>
</div>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-primary flex-shrink-0 transition-colors" />
</a>
))}
<a
href="https://planb.academy/en/tutorials/wallet"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-xs text-orange-400 hover:text-orange-300 transition-colors pt-1"
>
<ExternalLink className="h-3.5 w-3.5 flex-shrink-0" />
Learn more at Plan B Academy
</a>
</CardContent>
</Card>
);
}
// ─── Not Logged In ────────────────────────────────────────────────────────────
function NotLoggedInCard() {
return (
<Card className="border-dashed border-gray-700">
<CardContent className="py-12 px-6 text-center">
<div className="w-16 h-16 rounded-2xl bg-[#ff0040]/10 flex items-center justify-center mx-auto mb-4">
<Wallet className="h-8 w-8 text-[#ff0040]/60" />
</div>
<h3 className="text-lg font-bold mb-2">Login Required</h3>
<p className="text-muted-foreground text-sm max-w-xs mx-auto mb-6 leading-relaxed">
Log in with your Nostr account to connect your Lightning wallet and start zapping.
</p>
<LoginArea className="justify-center" />
</CardContent>
</Card>
);
}
// ─── Balance Hero Banner ──────────────────────────────────────────────────────
function BalanceHero() {
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
const cashuUsd = useSatsToUsd(cashuBalance);
const { webln } = useWallet();
const { connections } = useNWC();
const hasNWC = connections.length > 0 && connections.some(c => c.isConnected);
return (
<div className="relative overflow-hidden rounded-2xl border border-orange-500/30 bg-gradient-to-br from-orange-950/40 via-black to-yellow-950/20 p-5">
{/* Background glow */}
<div className="absolute -top-8 -right-8 w-32 h-32 bg-orange-500/10 rounded-full blur-2xl pointer-events-none" />
<div className="absolute -bottom-6 -left-6 w-24 h-24 bg-yellow-500/10 rounded-full blur-2xl pointer-events-none" />
<div className="relative flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Bitcoin className="h-4 w-4 text-orange-400" />
<span className="text-xs font-bold uppercase tracking-widest text-orange-400/80">Cashu Balance</span>
</div>
<div className="text-3xl font-black tabular-nums leading-none">
{hasCashu ? cashuBalance.toLocaleString() : '—'}
<span className="text-base font-semibold text-muted-foreground ml-2">sats</span>
</div>
{cashuUsd !== null && cashuBalance > 0 && (
<div className="text-sm text-muted-foreground mt-1">
≈ ${cashuUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
)}
{/* Wallet status pills */}
<div className="flex flex-wrap gap-1.5 mt-3">
{hasCashu && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-orange-500/20 border border-orange-500/30 text-[10px] font-bold text-orange-400">
<Nut className="h-2.5 w-2.5" /> Cashu
</span>
)}
{webln && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-500/20 border border-blue-500/30 text-[10px] font-bold text-blue-400">
<Globe className="h-2.5 w-2.5" /> WebLN
</span>
)}
{hasNWC && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-purple-500/20 border border-purple-500/30 text-[10px] font-bold text-purple-400">
<WalletMinimal className="h-2.5 w-2.5" /> NWC
</span>
)}
{!hasCashu && !webln && !hasNWC && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-gray-700/50 border border-gray-600 text-[10px] font-semibold text-gray-400">
No wallet connected
</span>
)}
</div>
</div>
{/* Manage button */}
<WalletModal>
<button className="flex-shrink-0 flex flex-col items-center gap-1.5 px-3 py-2.5 rounded-xl bg-orange-500/15 border border-orange-500/30 hover:bg-orange-500/25 active:scale-95 transition-all">
<Settings className="h-5 w-5 text-orange-400" />
<span className="text-[10px] font-bold text-orange-400 uppercase tracking-wide leading-none">Manage</span>
</button>
</WalletModal>
</div>
</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();
return (
<Layout>
<div className="container px-3 xs:px-4 py-4 md:py-8 max-w-2xl mx-auto">
{/* Header */}
<div className="mb-4">
<h1 className="text-2xl font-black uppercase tracking-wide flex items-center gap-2">
<Wallet className="h-6 w-6 text-orange-400" />
Wallet
</h1>
<p className="text-muted-foreground text-sm mt-0.5">
Bitcoin Lightning payments &amp; zaps
</p>
</div>
{user ? (
<div className="space-y-4">
{/* Balance hero — always at top for logged-in users */}
<BalanceHero />
{/* Quick Guide — collapsed by default */}
<WalletComparisonGuide />
<Tabs defaultValue="cashu" className="w-full">
{/* Tab pills — full width, thumb-friendly height */}
<TabsList className="grid w-full grid-cols-2 h-12 rounded-xl p-1 mb-4 bg-muted/60">
<TabsTrigger
value="cashu"
className="gap-2 rounded-lg text-sm font-bold h-full data-[state=active]:bg-orange-500 data-[state=active]:text-white"
>
<Nut className="h-4 w-4 flex-shrink-0" />
<span>Cashu</span>
</TabsTrigger>
<TabsTrigger
value="lightning"
className="gap-2 rounded-lg text-sm font-bold h-full data-[state=active]:bg-yellow-500 data-[state=active]:text-black"
>
<Zap className="h-4 w-4 flex-shrink-0" />
<span>Lightning</span>
</TabsTrigger>
</TabsList>
{/* Cashu tab */}
<TabsContent value="cashu" className="space-y-4 mt-0">
<CashuWalletCard />
</TabsContent>
{/* Lightning tab */}
<TabsContent value="lightning" className="space-y-4 mt-0">
<LightningPrivacyTips />
<WalletStatusCard />
<WalletResourcesCard />
<ZapsInfoCard />
</TabsContent>
</Tabs>
</div>
) : (
<div className="space-y-4">
<NotLoggedInCard />
<ZapsInfoCard />
</div>
)}
</div>
</Layout>
);
}