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>
This commit is contained in:
parent
9da1796774
commit
340f6fd89f
6 changed files with 489 additions and 429 deletions
|
|
@ -1,10 +1,36 @@
|
|||
v1.0.38 - Amber login fix (Capacitor native)
|
||||
v1.1.0 - Map as Homepage, Camera Fix, Sasquatch AI upgrades, UI/UX Polish
|
||||
|
||||
- Fixed login with Amber for ZapStore/Capacitor app users
|
||||
- Now uses the native nostr-signer-capacitor-plugin instead of browser
|
||||
intent URLs (which don't work inside a WebView)
|
||||
- Amber opens natively as a system intent, user approves, done
|
||||
- All event signing (posts, zaps, etc.) also goes through Amber natively
|
||||
- Fixed Android 11+ package visibility so Amber is detected correctly
|
||||
- Fixed Capacitor packages missing from package.json (prevented clean builds)
|
||||
- Added SNT4EVER as community partner and friend
|
||||
WHAT'S NEW
|
||||
|
||||
Map is now the Homepage
|
||||
- Opens directly to the interactive map on launch — no extra tap needed
|
||||
- HOME removed from nav, SPOTS is now the first tab
|
||||
- Map zoom capped: no more grey bands when zooming out, world stays visible
|
||||
|
||||
Camera Fixed for Native App
|
||||
- Tapping the camera button now opens your phone's native OS camera directly
|
||||
- No more browser permission errors inside the app
|
||||
- Upload from Gallery fallback added if camera is denied
|
||||
|
||||
Settings → My Stash
|
||||
- Quick link to your private gear inventory added to Settings > Wallet tab
|
||||
|
||||
Sasquatch AI Guide Upgrades
|
||||
- FAQ page is now powered by The Sasquatch — ask anything, get live answers
|
||||
- Quick question chips: tap a topic to instantly ask The Sasquatch
|
||||
- About page: Ask The Sasquatch button added
|
||||
- Sasquatch can now be triggered from anywhere in the app
|
||||
|
||||
UI/UX Polish
|
||||
- Wallet page fully reworked for mobile — better spacing, touch targets, readable on small screens
|
||||
- Focus rings are now on-brand red (accessibility compliant)
|
||||
- Card hover micro-interactions throughout the app
|
||||
- Consistent button/link transitions (150ms)
|
||||
- Reduced motion support for accessibility
|
||||
- Map min zoom capped — no more empty grey space when zooming out
|
||||
- Safe area insets improved for notched phones
|
||||
|
||||
BUG FIXES
|
||||
- Map no longer duplicates tiles when zoomed out too far
|
||||
- Camera no longer shows browser permission error in native app
|
||||
- Sasquatch panel now responds to external triggers from FAQ/About pages
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
Shield,
|
||||
ScanLine,
|
||||
Zap,
|
||||
Nut,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { QRScannerModal } from '@/components/wallet/QRScannerModal';
|
||||
|
|
@ -228,38 +229,42 @@ export function CashuWalletCard() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
{/* Main Wallet Card */}
|
||||
<Card className="lg:col-span-2 border-orange-500/30 bg-gradient-to-br from-orange-500/5 to-yellow-500/5">
|
||||
<CardHeader className="p-4 sm:p-6">
|
||||
<CardTitle className="text-lg sm:text-xl font-bold flex items-center gap-2">
|
||||
<Card className="border-orange-500/30 bg-gradient-to-br from-orange-500/5 to-yellow-500/5">
|
||||
<CardHeader className="p-4">
|
||||
<CardTitle className="text-base font-bold flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5 text-orange-500" />
|
||||
Cashu Wallet
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs sm:text-sm">
|
||||
Built-in ecash wallet - no external setup required
|
||||
<CardDescription className="text-xs">
|
||||
Built-in ecash wallet — no external setup required
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 sm:space-y-6 p-4 sm:p-6 pt-0 sm:pt-0">
|
||||
{/* Balance Display */}
|
||||
<div className="text-center py-4 sm:py-6 space-y-1 sm:space-y-2">
|
||||
<div className="text-xs sm:text-sm text-muted-foreground uppercase tracking-wide">Total Balance</div>
|
||||
<div className="text-3xl sm:text-4xl font-black">
|
||||
{totalBalance.toLocaleString()} <span className="text-xl sm:text-2xl text-muted-foreground">sats</span>
|
||||
</div>
|
||||
{usdValue !== null && (
|
||||
<div className="text-base sm:text-lg text-muted-foreground">
|
||||
≈ ${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
<CardContent className="space-y-4 p-4 pt-0">
|
||||
{/* Balance Display — compact on mobile since BalanceHero already shows it */}
|
||||
<div className="flex items-center justify-between py-3 px-4 rounded-xl bg-orange-500/10 border border-orange-500/20">
|
||||
<div>
|
||||
<div className="text-[10px] text-orange-400/80 uppercase tracking-widest font-bold mb-0.5">Balance</div>
|
||||
<div className="text-2xl font-black tabular-nums leading-none">
|
||||
{totalBalance.toLocaleString()}
|
||||
<span className="text-sm font-semibold text-muted-foreground ml-1.5">sats</span>
|
||||
</div>
|
||||
)}
|
||||
{usdValue !== null && (
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
≈ ${usdValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Nut className="h-8 w-8 text-orange-500/30 flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Active Mint Selector */}
|
||||
{mints.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Active Mint</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Active Mint</Label>
|
||||
<Select value={activeMintUrl || undefined} onValueChange={setActiveMintUrl}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-11">
|
||||
<SelectValue placeholder="Select a mint" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -280,12 +285,12 @@ export function CashuWalletCard() {
|
|||
|
||||
{/* Main Actions */}
|
||||
<Tabs defaultValue="receive" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="receive" className="gap-2">
|
||||
<TabsList className="grid w-full grid-cols-2 h-11 rounded-xl">
|
||||
<TabsTrigger value="receive" className="gap-2 rounded-lg h-full font-bold text-sm data-[state=active]:bg-green-600 data-[state=active]:text-white">
|
||||
<ArrowDownCircle className="h-4 w-4" />
|
||||
Receive
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="send" className="gap-2">
|
||||
<TabsTrigger value="send" className="gap-2 rounded-lg h-full font-bold text-sm data-[state=active]:bg-red-600 data-[state=active]:text-white">
|
||||
<ArrowUpCircle className="h-4 w-4" />
|
||||
Send
|
||||
</TabsTrigger>
|
||||
|
|
@ -295,19 +300,20 @@ export function CashuWalletCard() {
|
|||
<TabsContent value="receive" className="space-y-4 mt-4">
|
||||
{/* Lightning Invoice */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Receive Lightning Payment</Label>
|
||||
<div className="flex flex-col xs:flex-row gap-2">
|
||||
<Label className="text-sm font-semibold">⚡ Receive via Lightning</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder="Amount in sats"
|
||||
value={quoteAmount}
|
||||
onChange={(e) => setQuoteAmount(e.target.value)}
|
||||
className="min-w-0"
|
||||
className="h-11 min-w-0"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleLightningReceive}
|
||||
disabled={!activeMintUrl || !quoteAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
||||
>
|
||||
Generate Invoice
|
||||
</Button>
|
||||
|
|
@ -324,54 +330,55 @@ export function CashuWalletCard() {
|
|||
</div>
|
||||
|
||||
{/* Cashu Token */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Receive Cashu Token</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1"
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
>
|
||||
<ScanLine className="h-4 w-4" />
|
||||
Scan
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Paste Cashu token (cashu...)"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTokenReceive}
|
||||
disabled={!tokenInput}
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Receive Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">🥜 Receive Cashu Token</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-1.5 px-3"
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
>
|
||||
<ScanLine className="h-4 w-4" />
|
||||
Scan QR
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Paste Cashu token (cashu...)"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
className="font-mono text-xs h-11"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTokenReceive}
|
||||
disabled={!tokenInput}
|
||||
className="w-full h-11 bg-green-600 hover:bg-green-700 font-bold"
|
||||
>
|
||||
Receive Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Send Tab */}
|
||||
<TabsContent value="send" className="space-y-4 mt-4">
|
||||
{/* Send Token */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Create Cashu Token</Label>
|
||||
<div className="flex flex-col xs:flex-row gap-2">
|
||||
<Label className="text-sm font-semibold">🥜 Create Cashu Token</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder="Amount in sats"
|
||||
value={sendAmount}
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
className="min-w-0"
|
||||
className="min-w-0 h-11"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTokenSend}
|
||||
disabled={!activeMintUrl || !sendAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||
className="h-11 bg-orange-500 hover:bg-orange-600 font-bold"
|
||||
>
|
||||
Create Token
|
||||
</Button>
|
||||
|
|
@ -388,36 +395,36 @@ export function CashuWalletCard() {
|
|||
</div>
|
||||
|
||||
{/* Pay Lightning Invoice */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Pay Lightning Invoice</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1"
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
>
|
||||
<ScanLine className="h-4 w-4" />
|
||||
Scan
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Paste Lightning invoice (lnbc...)"
|
||||
value={lightningInvoice}
|
||||
onChange={(e) => setLightningInvoice(e.target.value)}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleLightningSend}
|
||||
disabled={!lightningInvoice || isProcessingPayment}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
{isProcessingPayment ? 'Processing...' : 'Pay Invoice'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">⚡ Pay Lightning Invoice</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 gap-1.5 px-3"
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
>
|
||||
<ScanLine className="h-4 w-4" />
|
||||
Scan QR
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Paste Lightning invoice (lnbc...)"
|
||||
value={lightningInvoice}
|
||||
onChange={(e) => setLightningInvoice(e.target.value)}
|
||||
className="font-mono text-xs h-11"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleLightningSend}
|
||||
disabled={!lightningInvoice || isProcessingPayment}
|
||||
className="w-full h-11 bg-blue-600 hover:bg-blue-700 font-bold"
|
||||
>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
{isProcessingPayment ? 'Processing...' : 'Pay Invoice'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
|
|
@ -429,79 +436,72 @@ export function CashuWalletCard() {
|
|||
</div>
|
||||
|
||||
{/* Pay Lightning Address */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Pay Lightning Address</Label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="user@domain.com"
|
||||
value={lightningAddress}
|
||||
onChange={(e) => setLightningAddress(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in sats"
|
||||
value={sendAmount}
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handlePayLightningAddress}
|
||||
disabled={!lightningAddress || !sendAmount || isResolvingAddress || isProcessingPayment}
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
{isResolvingAddress ? 'Resolving...' : 'Send'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">📧 Pay Lightning Address</Label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="user@domain.com"
|
||||
value={lightningAddress}
|
||||
onChange={(e) => setLightningAddress(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder="Amount in sats"
|
||||
value={sendAmount}
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button
|
||||
onClick={handlePayLightningAddress}
|
||||
disabled={!lightningAddress || !sendAmount || isResolvingAddress || isProcessingPayment}
|
||||
className="w-full h-11 bg-purple-600 hover:bg-purple-700 font-bold"
|
||||
>
|
||||
{isResolvingAddress ? 'Resolving...' : 'Send to Address'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* History */}
|
||||
{history.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
<History className="h-4 w-4" />
|
||||
Recent Activity
|
||||
</Label>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide flex items-center gap-2">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
Recent Activity
|
||||
</Label>
|
||||
<div className="space-y-2 max-h-52 overflow-y-auto scroll-container">
|
||||
{history.slice(0, 5).map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg bg-muted/50 border"
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-muted/50 border min-h-[52px]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
{entry.type === 'mint' || entry.type === 'receive' ? (
|
||||
<ArrowDownCircle className="h-4 w-4 text-green-500" />
|
||||
<ArrowDownCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ArrowUpCircle className="h-4 w-4 text-red-500" />
|
||||
<ArrowUpCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{entry.type === 'mint' && 'Received Lightning'}
|
||||
{entry.type === 'melt' && 'Sent Lightning'}
|
||||
{entry.type === 'send' && 'Sent Token'}
|
||||
{entry.type === 'receive' && 'Received Token'}
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold leading-tight">
|
||||
{entry.type === 'mint' && 'Received ⚡ Lightning'}
|
||||
{entry.type === 'melt' && 'Sent ⚡ Lightning'}
|
||||
{entry.type === 'send' && 'Sent 🥜 Token'}
|
||||
{entry.type === 'receive' && 'Received 🥜 Token'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(entry.timestamp).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-mono">
|
||||
<div className="text-right flex-shrink-0 ml-2">
|
||||
<div className={`text-sm font-mono font-bold ${entry.type === 'mint' || entry.type === 'receive' ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{entry.type === 'mint' || entry.type === 'receive' ? '+' : '-'}
|
||||
{entry.amount.toLocaleString()} sats
|
||||
{entry.amount.toLocaleString()}
|
||||
</div>
|
||||
<Badge
|
||||
variant={entry.status === 'completed' ? 'default' : entry.status === 'pending' ? 'secondary' : 'destructive'}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{entry.status}
|
||||
</Badge>
|
||||
<div className="text-xs text-muted-foreground">sats</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -512,34 +512,29 @@ export function CashuWalletCard() {
|
|||
</Card>
|
||||
|
||||
{/* What is Cashu Info Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<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 is Cashu?
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<CardContent className="px-4 pb-4 space-y-3">
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Cashu is digital cash backed by Bitcoin Lightning. Think of it as Bitcoin tokens you hold in your browser.
|
||||
</p>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-green-500">✅</span>
|
||||
<span className="text-muted-foreground">Works offline at skate spots</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-green-500">✅</span>
|
||||
<span className="text-muted-foreground">Instant peer-to-peer payments</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-green-500">✅</span>
|
||||
<span className="text-muted-foreground">Privacy-focused (mints don't track you)</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-green-500">✅</span>
|
||||
<span className="text-muted-foreground">Always redeemable for Lightning sats</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
||||
{[
|
||||
'Works offline at skate spots',
|
||||
'Instant peer-to-peer payments',
|
||||
"Privacy-focused (mints can't track you)",
|
||||
'Always redeemable for Lightning sats',
|
||||
].map((item) => (
|
||||
<div key={item} className="flex items-start gap-2 p-2.5 bg-muted/40 rounded-lg">
|
||||
<span className="text-green-500 flex-shrink-0 mt-0.5">✅</span>
|
||||
<span className="text-muted-foreground text-xs leading-snug">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href="https://cashumints.space/"
|
||||
|
|
@ -567,28 +562,28 @@ export function CashuWalletCard() {
|
|||
|
||||
{/* Backup Warning */}
|
||||
{mnemonic && (
|
||||
<Card className="mt-6 border-amber-500/50 bg-amber-500/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-amber-600">
|
||||
<Card className="border-amber-500/50 bg-amber-500/5">
|
||||
<CardHeader className="px-4 pt-4 pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-amber-600 text-base">
|
||||
<Shield className="h-5 w-5" />
|
||||
Backup Your Wallet
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your wallet is secured with a 12-word recovery phrase. Save it safely - you'll need it to restore your balance if you clear browser data.
|
||||
<CardContent className="px-4 pb-4 space-y-3">
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
Your wallet is secured with a 12-word recovery phrase. Save it safely — you'll need it to restore your balance if you clear browser data.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
alert(`Your backup phrase:\n\n${mnemonic}\n\nWrite these words down and store them safely!`);
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full border-amber-500/50 text-amber-600 hover:bg-amber-500/10"
|
||||
className="w-full h-11 border-amber-500/50 text-amber-600 hover:bg-amber-500/10 font-bold"
|
||||
>
|
||||
View Recovery Phrase
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
This is like your Nostr nsec - keep it secret and backed up!
|
||||
This is like your Nostr nsec — keep it secret and backed up!
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -413,11 +413,17 @@ html {
|
|||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
/* Extra small breakpoint for very small phones */
|
||||
/* Extra small breakpoint for very small phones (375px+) */
|
||||
@media (min-width: 375px) {
|
||||
.xs\:inline {
|
||||
display: inline;
|
||||
}
|
||||
.xs\:inline { display: inline; }
|
||||
.xs\:flex { display: flex; }
|
||||
.xs\:hidden { display: none; }
|
||||
.xs\:flex-row { flex-direction: row; }
|
||||
.xs\:px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.xs\:-mx-4 { margin-left: -1rem; margin-right: -1rem; }
|
||||
.xs\:text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.xs\:text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.xs\:gap-2 { gap: 0.5rem; }
|
||||
}
|
||||
|
||||
/* ─── Global UI/UX Polish ────────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -57,17 +57,17 @@ export default function FAQPage() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container px-4 py-12 max-w-3xl">
|
||||
<div className="container px-3 xs:px-4 py-6 sm:py-10 max-w-3xl mx-auto">
|
||||
|
||||
{/* Hero */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="inline-flex items-center justify-center w-24 h-24 rounded-3xl bg-gradient-to-br from-[#ff0040] via-[#ff6600] to-[#ccff00] mb-5 shadow-2xl shadow-[#ff0040]/40">
|
||||
<span className="text-5xl">🦧</span>
|
||||
<div className="text-center mb-7">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-2xl sm:rounded-3xl bg-gradient-to-br from-[#ff0040] via-[#ff6600] to-[#ccff00] mb-4 shadow-2xl shadow-[#ff0040]/40">
|
||||
<span className="text-3xl sm:text-4xl">🦧</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-black uppercase mb-3 tracking-tight">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-black uppercase mb-2 tracking-tight">
|
||||
<span className="text-white">Got Questions?</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-400 max-w-lg mx-auto leading-relaxed">
|
||||
<p className="text-base sm:text-xl text-gray-400 max-w-lg mx-auto leading-relaxed">
|
||||
Ask The Sasquatch — he knows everything about SkateSpots, Nostr, Bitcoin, and skateboarding.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -503,8 +503,8 @@ function WalletTab() {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<a href="/wallet">
|
||||
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 w-full sm:w-auto">
|
||||
<a href="/wallet" className="block sm:inline-block">
|
||||
<Button className="gap-2 bg-orange-500 hover:bg-orange-600 w-full sm:w-auto h-11 font-bold">
|
||||
<Wallet className="h-4 w-4" />
|
||||
Go to Wallet
|
||||
</Button>
|
||||
|
|
@ -545,7 +545,7 @@ function WalletTab() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<WalletModal>
|
||||
<Button variant="outline" className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10 w-full sm:w-auto">
|
||||
<Button variant="outline" className="gap-2 border-purple-500/50 text-purple-400 hover:bg-purple-500/10 w-full h-11 font-bold">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Connect with NWC
|
||||
</Button>
|
||||
|
|
@ -624,49 +624,51 @@ export default function SettingsPage() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container px-4 sm:px-6 py-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold flex items-center gap-3">
|
||||
<Settings className="h-8 w-8" />
|
||||
<div className="container px-3 xs:px-4 sm:px-6 py-4 sm:py-8 max-w-3xl mx-auto">
|
||||
<div className="mb-5">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-3">
|
||||
<Settings className="h-6 w-6 sm:h-8 sm:w-8" />
|
||||
Settings
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Manage your account and app preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue={initialTab} className="space-y-6">
|
||||
{/* Grid tabs for mobile, horizontal for larger screens */}
|
||||
<TabsList className="grid grid-cols-4 sm:grid-cols-7 gap-1 p-1 h-auto">
|
||||
<TabsTrigger value="profile" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<User className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Profile</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mydata" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Database className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">My Data</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Security</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="appearance" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Palette className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Theme</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="relays" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Radio className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Relays</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="wallet" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Wallet className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Wallet</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="advanced" className="flex-col sm:flex-row gap-1 sm:gap-1.5 px-2 py-2 sm:py-1.5 h-auto">
|
||||
<Wrench className="h-4 w-4" />
|
||||
<span className="text-[10px] sm:text-sm">Advanced</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs defaultValue={initialTab} className="space-y-5">
|
||||
{/* Horizontally scrollable tab bar on mobile — no wrapping */}
|
||||
<div className="overflow-x-auto scrollbar-hide -mx-3 xs:-mx-4 sm:mx-0 px-3 xs:px-4 sm:px-0">
|
||||
<TabsList className="inline-flex w-auto min-w-full sm:w-full sm:grid sm:grid-cols-7 gap-0.5 p-1 h-auto rounded-xl">
|
||||
<TabsTrigger value="profile" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<User className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Profile</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mydata" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Database className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">My Data</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Shield className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Security</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="appearance" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Palette className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Theme</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="relays" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Radio className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Relays</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="wallet" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Wallet className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Wallet</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="advanced" className="flex-col gap-1 px-3 py-2.5 h-auto min-w-[64px] sm:min-w-0 rounded-lg data-[state=active]:bg-background">
|
||||
<Wrench className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-[10px] font-semibold whitespace-nowrap">Advanced</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="profile">
|
||||
<ProfileTab />
|
||||
|
|
|
|||
|
|
@ -25,8 +25,12 @@ import {
|
|||
WalletMinimal,
|
||||
Info,
|
||||
Nut,
|
||||
ChevronRight,
|
||||
Bitcoin,
|
||||
} from 'lucide-react';
|
||||
|
||||
// ─── Wallet Status Card ───────────────────────────────────────────────────────
|
||||
|
||||
function WalletStatusCard() {
|
||||
const { webln } = useWallet();
|
||||
const { connections } = useNWC();
|
||||
|
|
@ -36,82 +40,85 @@ function WalletStatusCard() {
|
|||
const isReady = webln || hasNWC || hasCashu;
|
||||
|
||||
return (
|
||||
<Card className={isReady ? 'border-primary/30' : ''}>
|
||||
<CardHeader className="pb-3">
|
||||
<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">
|
||||
<Wallet className="h-5 w-5 text-primary" />
|
||||
<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'}>
|
||||
{isReady ? 'Ready' : 'Not Connected'}
|
||||
<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>
|
||||
<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="space-y-4">
|
||||
{/* Cashu Wallet Status */}
|
||||
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Nut className="h-5 w-5 text-orange-500" />
|
||||
<div>
|
||||
<p className="font-medium">Cashu Wallet</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<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)})` : ''}`
|
||||
? `${cashuBalance.toLocaleString()} sats${cashuUsd ? ` · $${cashuUsd.toFixed(2)}` : ''}`
|
||||
: 'Built-in ecash wallet'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{hasCashu ? (
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
{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 Status */}
|
||||
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">WebLN</p>
|
||||
<p className="text-sm text-muted-foreground">Browser extension wallet</p>
|
||||
{/* 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" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
{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 Status */}
|
||||
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<WalletMinimal className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">Nostr Wallet Connect</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{/* 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" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
{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">
|
||||
<Button className="w-full gap-2 mt-1 h-12 rounded-xl font-bold">
|
||||
<Settings className="h-4 w-4" />
|
||||
Manage Wallets
|
||||
</Button>
|
||||
|
|
@ -121,162 +128,112 @@ function WalletStatusCard() {
|
|||
);
|
||||
}
|
||||
|
||||
// ─── 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>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<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="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
Zaps are Lightning Network micropayments on Nostr. They allow you to send
|
||||
satoshis (sats) instantly to other users as a way of tipping, thanking, or
|
||||
supporting their content.
|
||||
<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="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg">⚡</span>
|
||||
<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>
|
||||
<p className="font-medium">Instant</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Payments settle in seconds over the Lightning Network
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg">🔐</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Flexible custody</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use your own wallet (self-custodial) or a custodial service - your choice
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-orange-500/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg">🥜</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">NutZaps for privacy</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use Cashu ecash for private zaps - mints can't see who pays whom
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg">💜</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Value for value</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Support creators directly for content you appreciate
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Recommended Wallets ──────────────────────────────────────────────────────
|
||||
|
||||
function WalletResourcesCard() {
|
||||
const walletResources = [
|
||||
{
|
||||
name: 'Alby',
|
||||
description: 'Browser extension wallet with WebLN + NWC support',
|
||||
url: 'https://getalby.com',
|
||||
type: 'WebLN',
|
||||
},
|
||||
{
|
||||
name: 'Minibits',
|
||||
description: 'Mobile Cashu wallet with NWC support',
|
||||
url: 'https://www.minibits.cash',
|
||||
type: 'NWC',
|
||||
},
|
||||
{
|
||||
name: 'Zeus',
|
||||
description: 'Self-custodial mobile Lightning wallet',
|
||||
url: 'https://zeusln.com',
|
||||
type: 'NWC',
|
||||
},
|
||||
{
|
||||
name: 'Coinos',
|
||||
description: 'Web-based Lightning wallet, works on any device',
|
||||
url: 'https://coinos.io',
|
||||
type: 'NWC',
|
||||
},
|
||||
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>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<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>
|
||||
Get started with one of these Lightning wallets
|
||||
</CardDescription>
|
||||
<CardDescription className="text-xs">Get started with one of these Lightning wallets</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3">
|
||||
{walletResources.map((wallet) => (
|
||||
<a
|
||||
key={wallet.name}
|
||||
href={wallet.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors group"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium group-hover:text-primary transition-colors">
|
||||
{wallet.name}
|
||||
</p>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{wallet.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{wallet.description}</p>
|
||||
<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>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</a>
|
||||
))}
|
||||
</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-sm text-orange-400 hover:text-orange-300 transition-colors"
|
||||
className="flex items-center gap-2 text-xs text-orange-400 hover:text-orange-300 transition-colors pt-1"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<span>Want to learn more? Check out Plan B Academy's wallet guides</span>
|
||||
<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">
|
||||
<CardContent className="py-12 text-center">
|
||||
<Wallet className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
|
||||
<h3 className="text-lg font-medium mb-2">Login Required</h3>
|
||||
<p className="text-muted-foreground max-w-md mx-auto mb-6">
|
||||
<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" />
|
||||
|
|
@ -285,6 +242,75 @@ function NotLoggedInCard() {
|
|||
);
|
||||
}
|
||||
|
||||
// ─── 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',
|
||||
|
|
@ -295,58 +321,63 @@ export default function WalletPage() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container px-4 py-6 md:py-8">
|
||||
<div className="container px-3 xs:px-4 py-4 md:py-8 max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-6 md:mb-8">
|
||||
<h1 className="text-2xl md:text-3xl font-bold">Wallet</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm md:text-base">
|
||||
Manage your Bitcoin and send zaps to skaters and creators
|
||||
<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 & zaps
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Wallet Comparison Guide */}
|
||||
<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">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4 md:mb-6 h-auto">
|
||||
<TabsTrigger value="cashu" className="gap-1.5 py-2.5 text-sm">
|
||||
{/* 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 Wallet</span>
|
||||
<span>Cashu</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="lightning" className="gap-1.5 py-2.5 text-sm">
|
||||
<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>
|
||||
|
||||
<TabsContent value="cashu" className="space-y-6">
|
||||
<CashuWalletCard />
|
||||
</TabsContent>
|
||||
{/* Cashu tab */}
|
||||
<TabsContent value="cashu" className="space-y-4 mt-0">
|
||||
<CashuWalletCard />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lightning" className="space-y-6">
|
||||
{/* Privacy Tips at the top */}
|
||||
<LightningPrivacyTips />
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<WalletStatusCard />
|
||||
<WalletResourcesCard />
|
||||
</div>
|
||||
<div>
|
||||
<ZapsInfoCard />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{/* Lightning tab */}
|
||||
<TabsContent value="lightning" className="space-y-4 mt-0">
|
||||
<LightningPrivacyTips />
|
||||
<WalletStatusCard />
|
||||
<WalletResourcesCard />
|
||||
<ZapsInfoCard />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-xl mx-auto">
|
||||
<div className="space-y-4">
|
||||
<NotLoggedInCard />
|
||||
<div className="mt-6">
|
||||
<ZapsInfoCard />
|
||||
</div>
|
||||
<ZapsInfoCard />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue