Files
telegram-shop/src/components/wallets/wallets-page.tsx
Z User 482e8cf444 feat: styling polish, command palette, breadcrumbs, activity feed, export, sort, clipboard
- 40+ styling fixes across all 11 pages (consistent padding, titles, empty states, date formatting)
- Command Palette (Ctrl+K) with 11 nav items + 3 actions
- Breadcrumbs with hash-based path detection and mobile truncation
- Activity Feed component with 11 color-coded action icons and 30s auto-refresh
- ExportButton (CSV) on Purchases, Audit, Users pages
- SortableHeader with 3-state toggle on Purchases and Audit pages
- copyToClipboard utility with navigator.clipboard + fallback
- Notification badge on Purchases sidebar item (pending count)
- Connection status indicator in sidebar footer
- Removed dynamic imports from page.tsx (reduces memory)
- Disabled Prisma query logging (reduces memory)
- All components verified: named exports, @/ prefix, hash navigation, sonner toasts
2026-08-05 13:40:32 +00:00

914 lines
39 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { useAuthStore } from '@/stores/auth-store';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import {
Wallet,
Users,
Eye,
EyeOff,
Search,
RefreshCw,
DollarSign,
Download,
Lock,
Copy,
AlertTriangle,
ShieldCheck,
} from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
// ── Types ──────────────────────────────────────────────
interface WalletUser {
id: number;
username: string | null;
telegramId: string;
status: number;
totalBalance: number;
bonusBalance: number;
walletCount: number;
country: string | null;
city: string | null;
}
interface UserWallet {
id: number;
walletType: string;
address: string;
balance: number;
createdAt: string;
}
interface UserProfile {
id: number;
username: string | null;
telegramId: string;
status: number;
totalBalance: number;
bonusBalance: number;
country: string | null;
city: string | null;
createdAt: string;
wallets: UserWallet[];
}
interface OverviewData {
totals: Record<string, number>;
walletCounts: Record<string, number>;
totalUsd: number;
totalWallets: number;
totalUsers: number;
commissionEnabled: boolean;
commissionRate: number;
currentCommission: number;
payments: {
id: number;
paidAmountUsd: number;
note: string | null;
createdAt: string;
}[];
lastPaidAmount: number;
commissionDue: number;
}
interface SeedEntry {
walletId: number;
userId: number;
username: string;
walletType: string;
address: string;
derivationPath: string;
mnemonic: string;
}
// ── Helpers ────────────────────────────────────────────
function formatBalance(val: number): string {
return val.toFixed(8);
}
function truncateAddress(addr: string, len = 10): string {
if (addr.length <= len * 2) return addr;
return `${addr.slice(0, len)}...${addr.slice(-len)}`;
}
function statusBadge(status: number) {
if (status === 0) return <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100">Active</Badge>;
if (status === 2) return <Badge className="bg-red-100 text-red-700 hover:bg-red-100">Blocked</Badge>;
return <Badge variant="secondary">Unknown</Badge>;
}
function walletTypeBadge(type: string) {
const colorMap: Record<string, string> = {
BTC: 'bg-orange-100 text-orange-700 hover:bg-orange-100',
LTC: 'bg-gray-100 text-gray-700 hover:bg-gray-100',
ETH: 'bg-violet-100 text-violet-700 hover:bg-violet-100',
USDT: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-100',
USDC: 'bg-blue-100 text-blue-700 hover:bg-blue-100',
};
return <Badge className={colorMap[type] || ''}>{type}</Badge>;
}
function copyToClipboard(text: string, label: string) {
navigator.clipboard.writeText(text).then(() => {
toast.success(`${label} copied!`);
}).catch(() => {
toast.error('Failed to copy');
});
}
// ── Component ───────────────────────────────────────────
export function WalletsPage() {
const { role } = useAuthStore();
const isSuperAdmin = role === 'super_admin';
// Tab 1 state
const [userList, setUserList] = useState<WalletUser[]>([]);
const [userListLoading, setUserListLoading] = useState(true);
const [userSearch, setUserSearch] = useState('');
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
const [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
const [selectedUserLoading, setSelectedUserLoading] = useState(false);
// Tab 2 state
const [overview, setOverview] = useState<OverviewData | null>(null);
const [overviewLoading, setOverviewLoading] = useState(true);
const [paymentAmount, setPaymentAmount] = useState('');
const [paymentNote, setPaymentNote] = useState('');
const [paymentSubmitting, setPaymentSubmitting] = useState(false);
// Tab 3 state
const [seeds, setSeeds] = useState<SeedEntry[]>([]);
const [seedsLoading, setSeedsLoading] = useState(false);
const [seedsLoaded, setSeedsLoaded] = useState(false);
const [revealedMnemonics, setRevealedMnemonics] = useState<Set<number>>(new Set());
const [seedsAlertOpen, setSeedsAlertOpen] = useState(false);
const [exportAlertOpen, setExportAlertOpen] = useState(false);
// ── Tab 1: User Wallets ────────────────────────────
const fetchUserList = useCallback(async (search?: string) => {
setUserListLoading(true);
try {
const query = search ? `?search=${encodeURIComponent(search)}` : '';
const res = await fetch(`/api/wallets/bulk${query}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setUserList(data);
} catch {
toast.error('Failed to load user list');
} finally {
setUserListLoading(false);
}
}, []);
useEffect(() => {
const debounce = setTimeout(() => {
fetchUserList(userSearch);
}, 300);
return () => clearTimeout(debounce);
}, [userSearch, fetchUserList]);
const selectUser = async (userId: number) => {
setSelectedUserId(userId);
setSelectedUser(null);
setSelectedUserLoading(true);
try {
const res = await fetch(`/api/wallets/${userId}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setSelectedUser(data);
} catch {
toast.error('Failed to load user wallets');
} finally {
setSelectedUserLoading(false);
}
};
// ── Tab 2: Owner Summary ──────────────────────────
useEffect(() => {
const fetchOverview = async () => {
setOverviewLoading(true);
try {
const res = await fetch('/api/wallets/overview');
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setOverview(data);
} catch {
toast.error('Failed to load overview');
} finally {
setOverviewLoading(false);
}
};
fetchOverview();
}, []);
const handleRecordPayment = async () => {
const amount = parseFloat(paymentAmount);
if (isNaN(amount) || amount <= 0) {
toast.error('Enter a valid amount');
return;
}
setPaymentSubmitting(true);
try {
const res = await fetch('/api/wallets/record-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paidAmount: amount, note: paymentNote }),
});
if (!res.ok) throw new Error('Failed');
toast.success('Payment recorded successfully');
setPaymentAmount('');
setPaymentNote('');
// Refetch overview
const overviewRes = await fetch('/api/wallets/overview');
if (overviewRes.ok) {
const data = await overviewRes.json();
setOverview(data);
}
} catch {
toast.error('Failed to record payment');
} finally {
setPaymentSubmitting(false);
}
};
// ── Tab 3: Seed Phrases ────────────────────────────
const handleLoadSeeds = async () => {
setSeedsLoading(true);
try {
const res = await fetch('/api/wallets/seeds');
if (!res.ok) throw new Error('Failed');
const data = await res.json();
setSeeds(data);
setSeedsLoaded(true);
toast.success(`Loaded ${data.length} seed phrases`);
} catch {
toast.error('Failed to load seeds');
} finally {
setSeedsLoading(false);
setSeedsAlertOpen(false);
}
};
const handleExportSeeds = () => {
window.open('/api/wallets/export-seeds', '_blank');
setExportAlertOpen(false);
};
const toggleMnemonic = (walletId: number) => {
setRevealedMnemonics((prev) => {
const next = new Set(prev);
if (next.has(walletId)) {
next.delete(walletId);
} else {
next.add(walletId);
}
return next;
});
};
// ── Render ─────────────────────────────────────────
const currencyTypes = ['BTC', 'LTC', 'ETH', 'USDT', 'USDC'];
return (
<div className="p-4 md:p-6 space-y-6">
<div className="flex items-center gap-3">
<Wallet className="h-8 w-8 text-orange-500" />
<div>
<h2 className="text-xl font-semibold">Wallets</h2>
<p className="text-sm text-muted-foreground">Manage crypto wallets, view balances, and track commissions</p>
</div>
</div>
<Tabs defaultValue="user-wallets" className="w-full">
<TabsList className="grid w-full grid-cols-3 max-w-lg">
<TabsTrigger value="user-wallets" className="gap-2">
<Users className="h-4 w-4" />
User Wallets
</TabsTrigger>
<TabsTrigger value="owner-summary" className="gap-2">
<DollarSign className="h-4 w-4" />
Owner Summary
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="seed-phrases" className="gap-2">
<ShieldCheck className="h-4 w-4" />
Seeds
</TabsTrigger>
)}
</TabsList>
{/* ── Tab 1: User Wallets ─────────────────────── */}
<TabsContent value="user-wallets" className="mt-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: User List */}
<Card className="lg:col-span-1">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Users with Wallets</CardTitle>
<div className="relative mt-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search username or Telegram ID..."
className="pl-9"
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="max-h-[calc(100vh-12rem)] overflow-y-auto">
{userListLoading ? (
<div className="p-4 space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between p-3">
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
<Skeleton className="h-5 w-12" />
</div>
))}
</div>
) : userList.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<Users className="h-10 w-10 mx-auto mb-2 opacity-40" />
<p>No users with wallets found</p>
</div>
) : (
<div className="divide-y">
{userList.map((u) => (
<button
key={u.id}
className={`w-full text-left px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors ${
selectedUserId === u.id ? 'bg-muted border-l-2 border-l-orange-500' : ''
}`}
onClick={() => selectUser(u.id)}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{u.username || `User#${u.id}`}
</span>
{statusBadge(u.status)}
</div>
<p className="text-xs text-muted-foreground mt-0.5">
TG: {u.telegramId} · {u.country ? `${u.country}${u.city ? `, ${u.city}` : ''}` : 'N/A'}
</p>
</div>
<Badge variant="outline" className="ml-2 shrink-0">
{u.walletCount} wallet{u.walletCount !== 1 ? 's' : ''}
</Badge>
</button>
))}
</div>
)}
</div>
</CardContent>
</Card>
{/* Right: Selected User Details */}
<div className="lg:col-span-2 space-y-4">
{selectedUserLoading && (
<>
<Card>
<CardHeader>
<Skeleton className="h-6 w-40" />
<Skeleton className="h-4 w-60" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-16" />
))}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 space-y-3">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
</>
)}
{!selectedUserLoading && !selectedUser && (
<Card className="flex items-center justify-center min-h-[300px]">
<CardContent className="text-center py-16">
<Wallet className="h-12 w-12 mx-auto mb-3 text-muted-foreground/40" />
<p className="text-muted-foreground">Select a user from the list to view their wallets</p>
</CardContent>
</Card>
)}
{!selectedUserLoading && selectedUser && (
<>
{/* Profile Card */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
{selectedUser.username || `User#${selectedUser.id}`}
</CardTitle>
<div className="flex items-center gap-2">
{statusBadge(selectedUser.status)}
<Button
variant="outline"
size="sm"
className="gap-1"
onClick={() => {
toast.success('Balances refreshed');
}}
>
<RefreshCw className="h-3.5 w-3.5" />
Refresh Balances
</Button>
</div>
</div>
<CardDescription>Telegram ID: {selectedUser.telegramId}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Main Balance</p>
<p className="text-lg font-semibold">${selectedUser.totalBalance.toFixed(2)}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Bonus Balance</p>
<p className="text-lg font-semibold">${selectedUser.bonusBalance.toFixed(2)}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Country</p>
<p className="text-sm font-medium mt-0.5">{selectedUser.country || '—'}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">City</p>
<p className="text-sm font-medium mt-0.5">{selectedUser.city || '—'}</p>
</div>
</div>
</CardContent>
</Card>
{/* Wallets Table */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Wallets ({selectedUser.wallets.length})</CardTitle>
</CardHeader>
<CardContent className="p-0">
{selectedUser.wallets.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<p>No wallets found for this user</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Address</TableHead>
<TableHead className="text-right">Balance</TableHead>
<TableHead className="text-right">Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedUser.wallets.map((w) => (
<TableRow key={w.id}>
<TableCell>{walletTypeBadge(w.walletType)}</TableCell>
<TableCell>
<button
className="font-mono text-xs hover:text-orange-500 transition-colors cursor-pointer"
onClick={() => copyToClipboard(w.address, 'Address')}
title="Click to copy"
>
{truncateAddress(w.address)}
</button>
</TableCell>
<TableCell className="text-right font-mono text-sm">
{formatBalance(w.balance)}
</TableCell>
<TableCell className="text-right text-xs text-muted-foreground">
{new Date(w.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</>
)}
</div>
</div>
</TabsContent>
{/* ── Tab 2: Owner Summary ────────────────────── */}
<TabsContent value="owner-summary" className="mt-6 space-y-6">
{overviewLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-28" />
))}
</div>
) : overview ? (
<>
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">Total USD Value</p>
<DollarSign className="h-4 w-4 text-orange-500" />
</div>
<p className="text-2xl font-bold mt-1">${overview.totalUsd.toFixed(2)}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">Total Users</p>
<Users className="h-4 w-4 text-emerald-500" />
</div>
<p className="text-2xl font-bold mt-1">{overview.totalUsers}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">Active Wallets</p>
<Wallet className="h-4 w-4 text-violet-500" />
</div>
<p className="text-2xl font-bold mt-1">{overview.totalWallets}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">Commission Due</p>
<DollarSign className="h-4 w-4 text-red-500" />
</div>
<p className="text-2xl font-bold mt-1">${overview.commissionDue.toFixed(2)}</p>
</CardContent>
</Card>
</div>
{/* Balances by Currency */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Balances by Currency</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Currency</TableHead>
<TableHead className="text-right">Wallet Count</TableHead>
<TableHead className="text-right">Total Balance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currencyTypes.map((type) => (
<TableRow key={type}>
<TableCell>{walletTypeBadge(type)}</TableCell>
<TableCell className="text-right">
{overview.walletCounts[type] || 0}
</TableCell>
<TableCell className="text-right font-mono text-sm">
{formatBalance(overview.totals[type] || 0)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Commission Section */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Commission Management</CardTitle>
<CardDescription>
Current rate: {(overview.commissionRate * 100).toFixed(0)}%
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Commission Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Rate</p>
<p className="text-lg font-semibold">{(overview.commissionRate * 100).toFixed(0)}%</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Total Balance</p>
<p className="text-lg font-semibold">${overview.totalUsd.toFixed(2)}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Commission Amount</p>
<p className="text-lg font-semibold">${overview.currentCommission.toFixed(2)}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground">Last Paid</p>
<p className="text-lg font-semibold">${overview.lastPaidAmount.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg border bg-orange-50 text-orange-700 dark:bg-orange-950 dark:text-orange-300">
<DollarSign className="h-4 w-4 shrink-0" />
<span className="text-sm font-medium">Commission Due: ${overview.commissionDue.toFixed(2)}</span>
</div>
{/* Record Payment Form */}
<div className="space-y-3">
<h4 className="text-sm font-medium">Record Payment</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<Label htmlFor="payment-amount">Amount (USD)</Label>
<Input
id="payment-amount"
type="number"
step="0.01"
min="0"
placeholder="0.00"
value={paymentAmount}
onChange={(e) => setPaymentAmount(e.target.value)}
/>
</div>
<div>
<Label htmlFor="payment-note">Note</Label>
<Input
id="payment-note"
placeholder="Payment note (optional)"
value={paymentNote}
onChange={(e) => setPaymentNote(e.target.value)}
/>
</div>
<div className="flex items-end">
<Button
onClick={handleRecordPayment}
disabled={paymentSubmitting}
className="w-full"
>
{paymentSubmitting ? 'Recording...' : 'Record Payment'}
</Button>
</div>
</div>
</div>
{/* Payment History */}
<div>
<h4 className="text-sm font-medium mb-3">Payment History</h4>
{overview.payments.length === 0 ? (
<div className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
No payment records yet
</div>
) : (
<div className="max-h-72 overflow-y-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead className="text-right">Amount (USD)</TableHead>
<TableHead>Note</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{overview.payments.map((p) => (
<TableRow key={p.id}>
<TableCell className="text-xs text-muted-foreground">
{new Date(p.createdAt).toLocaleDateString()}
</TableCell>
<TableCell className="text-right font-mono">
${p.paidAmountUsd.toFixed(2)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{p.note || '—'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
</CardContent>
</Card>
</>
) : null}
</TabsContent>
{/* ── Tab 3: Seed Phrases ────────────────────── */}
<TabsContent value="seed-phrases" className="mt-6 space-y-4">
{isSuperAdmin && (
<>
{/* Commission Warning */}
{overview && overview.commissionDue > 0 && (
<div className="flex items-start gap-3 rounded-lg border border-orange-300 bg-orange-50 p-4 dark:bg-orange-950 dark:border-orange-800">
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-orange-700 dark:text-orange-300">
Outstanding Commission: ${overview.commissionDue.toFixed(2)}
</p>
<p className="text-xs text-orange-600 dark:text-orange-400 mt-1">
Please pay the outstanding commission before accessing seed phrases.
</p>
</div>
</div>
)}
{/* Action Buttons */}
<Card>
<CardContent className="p-4">
<div className="flex flex-wrap gap-3">
<AlertDialog open={seedsAlertOpen} onOpenChange={setSeedsAlertOpen}>
<AlertDialogTrigger asChild>
<Button
variant="outline"
className="gap-2"
disabled={overview && overview.commissionDue > 0}
>
<Lock className="h-4 w-4" />
{seedsLoaded ? 'Reload Seed Phrases' : 'Unlock & Load Seed Phrases'}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> Confirm Access to Seed Phrases</AlertDialogTitle>
<AlertDialogDescription>
You are about to access sensitive cryptographic seed phrases. This action will be
logged in the audit trail. Only proceed if you are authorized.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleLoadSeeds}>
Proceed
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{seedsLoaded && (
<AlertDialog open={exportAlertOpen} onOpenChange={setExportAlertOpen}>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="gap-2">
<Download className="h-4 w-4" />
Export All Seeds as CSV
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> Confirm CSV Export</AlertDialogTitle>
<AlertDialogDescription>
This will download all seed phrases as a CSV file containing sensitive data.
The export will be logged in the audit trail. Ensure you handle this file securely.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleExportSeeds}>
Export CSV
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</CardContent>
</Card>
{/* Seeds Table */}
{seedsLoading && (
<Card>
<CardContent className="p-4 space-y-3">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</CardContent>
</Card>
)}
{!seedsLoading && seedsLoaded && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Seed Phrases ({seeds.length})</CardTitle>
<CardDescription>Sensitive data handle with care</CardDescription>
</CardHeader>
<CardContent className="p-0">
{seeds.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<Lock className="h-10 w-10 mx-auto mb-2 opacity-40" />
<p>No seed phrases found</p>
</div>
) : (
<div className="max-h-[calc(100vh-28rem)] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Type</TableHead>
<TableHead>Address</TableHead>
<TableHead>Derivation</TableHead>
<TableHead>Mnemonic</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{seeds.map((s) => (
<TableRow key={s.walletId}>
<TableCell className="text-sm font-medium">
{s.username}
</TableCell>
<TableCell>{walletTypeBadge(s.walletType)}</TableCell>
<TableCell>
<button
className="font-mono text-xs hover:text-orange-500 transition-colors cursor-pointer"
onClick={() => copyToClipboard(s.address, 'Address')}
title="Click to copy"
>
{truncateAddress(s.address)}
</button>
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground">
{s.derivationPath || '—'}
</TableCell>
<TableCell className="max-w-[280px]">
<div className="flex items-center gap-1">
<span className="font-mono text-xs truncate flex-1">
{revealedMnemonics.has(s.walletId)
? s.mnemonic
: '••••••••••••••••'}
</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
onClick={() => toggleMnemonic(s.walletId)}
>
{revealedMnemonics.has(s.walletId) ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
onClick={() => copyToClipboard(s.mnemonic, 'Mnemonic')}
>
<Copy className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
)}
{!seedsLoading && !seedsLoaded && (
<Card className="flex items-center justify-center min-h-[300px]">
<CardContent className="text-center py-16">
<Lock className="h-12 w-12 mx-auto mb-3 text-muted-foreground/40" />
<p className="text-muted-foreground">
Click &quot;Unlock &amp; Load Seed Phrases&quot; to view sensitive data
</p>
</CardContent>
</Card>
)}
</>
)}
</TabsContent>
</Tabs>
</div>
);
}