refactor(web): split AdminDashboardView into admin-dashboard-view module

- types: DashboardStats, UploadItem, AuditLogItem, StatCardProps, Report
- useAdminDashboardView: fetchData, handleAction, triggerProtocol
- Header, StatCard, TrafficCard, ProtocolsCard, NodeHealthCard, Tabs
- AdminDashboardSkeleton for Loading state
- max-w-layout-content, text-xs, gap-0.5 (no arbitrary values)
- Stories: Default, Loading (Skeleton). Decorator min-h-layout-page
- Re-export from AdminDashboardView.tsx

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-06 17:54:02 +01:00
parent 27db3ef8ed
commit fb3e5ceb5b
13 changed files with 797 additions and 322 deletions

View file

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminDashboardView } from './AdminDashboardView';
import { AdminDashboardSkeleton } from './admin-dashboard-view';
import { ToastProvider } from '../../components/feedback/ToastProvider';
/**
@ -9,48 +10,44 @@ import { ToastProvider } from '../../components/feedback/ToastProvider';
* visualisation du trafic, queue de modération et logs système.
*/
const meta: Meta<typeof AdminDashboardView> = {
title: 'Components/Features/Admin/AdminDashboardView',
component: AdminDashboardView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
},
},
title: 'Components/Features/Admin/AdminDashboardView',
component: AdminDashboardView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component:
'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
</ToastProvider>
),
],
},
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-layout-page p-4">
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec données chargées.
*/
export const Default: Story = {
name: 'Par défaut',
name: 'Par défaut',
};
/**
* État de chargement initial.
*/
export const Loading: Story = {
name: 'Chargement',
parameters: {
docs: {
description: {
story: 'Affiche le spinner pendant le chargement des données admin.',
},
},
name: 'Chargement',
render: () => <AdminDashboardSkeleton />,
parameters: {
docs: {
description: {
story: 'Skeleton pendant le chargement des données admin.',
},
},
},
};

View file

@ -1,289 +1 @@
import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import {
Users, DollarSign, Activity, AlertTriangle, HardDrive, ShoppingBag, ShieldAlert, Loader2, Server, Database, Lock, RefreshCw, Eye, ShieldCheck, History
} from 'lucide-react';
import { adminService } from '../../services/adminService';
import { Report } from '../../types';
import { useToast } from '../../components/feedback/ToastProvider';
import { logger } from '@/utils/logger';
import { cn } from '@/lib/utils';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
const NebulaStatCard = ({ label, value, icon, color, trend }: any) => (
<Card variant="glass" className="p-5 relative overflow-hidden group hover:border-primary/50 transition-all cursor-default">
<div className={cn("absolute -right-4 -top-4 w-24 h-24 rounded-full opacity-10 blur-2xl transition-all group-hover:opacity-20", `bg-${color}-500`)} />
<div className="flex justify-between items-start mb-3 relative z-10">
<div className={cn("p-2.5 rounded-xl bg-white/5 shadow-inner", `text-${color}-500`)}>{icon}</div>
{trend !== undefined && (
<span className={cn("text-[10px] font-mono font-bold px-2 py-0.5 rounded-full border tracking-tighter", trend > 0 ? "text-lime-500 border-lime-500/20 bg-lime-500/10" : "text-red-500 border-red-500/20 bg-red-500/10")}>
{trend > 0 ? '+' : ''}{trend}%
</span>
)}
</div>
<div className="text-3xl font-display font-bold text-white mb-1 relative z-10">{value}</div>
<div className="text-[10px] text-muted-foreground uppercase tracking-[0.2em] font-mono relative z-10">{label}</div>
</Card>
);
export const AdminDashboardView: React.FC = () => {
const { addToast } = useToast();
const [stats, setStats] = useState<any>({});
const [reports, setReports] = useState<Report[]>([]);
const [uploads, setUploads] = useState<any[]>([]);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [protocolActive, setProtocolActive] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const [statsData, reportsData, uploadsData, logsData] = await Promise.all([
adminService.getDashboardStats(),
adminService.getModerationQueue('pending'),
adminService.getRecentUploads(),
adminService.getAuditLogs({ limit: 10 }),
]);
setStats(statsData);
setReports(reportsData);
setUploads(uploadsData);
setAuditLogs(logsData.logs || []);
} catch (e) {
logger.error('Error loading admin dashboard', { error: e });
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleAction = async (id: string, action: string) => {
await adminService.resolveReport(id, action);
setReports(reports.filter((r) => r.id !== id));
addToast(`Protocol "${action.toUpperCase()}" executed successfully.`, 'success');
};
const triggerProtocol = (name: string, color: string) => {
setProtocolActive(name);
addToast(`INITIALIZING ${name.toUpperCase()}...`, 'info');
setTimeout(() => {
addToast(`${name.toUpperCase()} DEPLOYED`, color as any);
setProtocolActive(null);
}, 2000);
};
if (loading) return <div className="flex justify-center py-24"><Loader2 className="w-10 h-10 text-primary animate-spin" /></div>;
return (
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-[1600px]">
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
<div>
<h2 className="text-4xl font-display font-bold text-white mb-2 flex items-center gap-3">
<ShieldCheck className="text-primary w-10 h-10" /> COMMAND CENTER
</h2>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-lime-500 animate-pulse shadow-glow-lime" />
<span className="text-muted-foreground text-[10px] font-mono tracking-widest uppercase">Nodes Online</span>
</div>
<div className="h-3 w-px bg-white/10" />
<span className="text-muted-foreground text-[10px] font-mono tracking-widest uppercase">Sector: 00-ALPHA</span>
</div>
</div>
<div className="flex gap-3">
<Button variant="outline" className="border-white/10 hover:bg-white/5 font-mono text-[10px] h-10 tracking-widest" onClick={() => triggerProtocol('RESCAN', 'success')}>
<RefreshCw className={cn("w-3 h-3 mr-2", protocolActive === 'RESCAN' && "animate-spin")} /> FULL RESCAN
</Button>
<Button variant="outline" className="border-red-500/20 text-red-500 hover:bg-red-500/10 font-mono text-[10px] h-10 tracking-widest" onClick={() => triggerProtocol('LOCKDOWN', 'error')}>
<Lock className="w-3 h-3 mr-2" /> LOCKDOWN
</Button>
</div>
</div>
{/* Primary Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<NebulaStatCard label="Total Nodes" value={stats.totalUsers?.toLocaleString()} icon={<Users className="w-5 h-5" />} trend={stats.trends?.users} color="cyan" />
<NebulaStatCard label="Credit Volume" value={`$${stats.monthlyRevenue?.toLocaleString()}`} icon={<DollarSign className="w-5 h-5" />} trend={stats.trends?.revenue} color="gold" />
<NebulaStatCard label="Active Uplinks" value={stats.activeSessions?.toLocaleString()} icon={<Activity className="w-5 h-5" />} trend={stats.trends?.sessions} color="lime" />
<NebulaStatCard label="Threat Reports" value={stats.pendingReports} icon={<ShieldAlert className="w-5 h-5" />} trend={stats.trends?.reports} color="red" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Real-time Traffic Visualizer */}
<Card variant="glass" className="lg:col-span-2 p-8 bg-black/40 border-white/5 relative overflow-hidden">
<div className="flex justify-between items-center mb-10 relative z-10">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-3">
<Server className="w-5 h-5 text-primary" /> Traffic Flux
</h3>
<p className="text-[10px] text-muted-foreground font-mono mt-1">HOLOGRAPHIC STREAMING INTERFACE</p>
</div>
<div className="flex gap-8 text-[10px] font-mono uppercase tracking-[0.2em] italic">
<span className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-cyan-500 shadow-glow-cyan" /> Uplink</span>
<span className="flex items-center gap-2"><div className="w-2 h-2 rounded-full bg-magenta-500 shadow-glow-magenta" /> Downlink</span>
</div>
</div>
<div className="h-64 flex items-end gap-[2px] px-2 border-b border-l border-white/10 relative bg-black/30 rounded-lg overflow-hidden group">
{/* Holographic scanning line */}
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-transparent w-24 h-full animate-scan pointer-events-none" />
<div className="absolute inset-0 grid grid-rows-4 w-full h-full pointer-events-none">
{[...Array(4)].map((_, i) => <div key={i} className="border-t border-white/5 w-full" />)}
</div>
{Array.from({ length: 60 }).map((_, i) => {
const h1 = Math.random() * 50 + 5;
const h2 = Math.random() * 30 + 10;
return (
<div key={i} className="flex-1 flex flex-col justify-end h-full opacity-60 group-hover:opacity-100 transition-opacity">
<div className="w-full bg-magenta-500/50 hover:bg-magenta-500 transition-all rounded-t-sm" style={{ height: `${h2}%` }} />
<div className="w-full bg-cyan-500/50 hover:bg-cyan-500 transition-all rounded-t-sm" style={{ height: `${h1}%` }} />
</div>
);
})}
</div>
<div className="flex justify-between mt-4 text-[10px] font-mono text-muted-foreground uppercase opacity-50 px-2 tracking-tighter">
<span>SYS_INIT</span>
<span>BUFFERING_NODES...</span>
<span>LIVE_DATA</span>
</div>
</Card>
{/* Global Control Terminal */}
<div className="space-y-6">
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-[0.3em] mb-6">Protocols</h3>
<div className="grid grid-cols-2 gap-4">
{[
{ id: 'purge', label: 'PURGE CACHE', icon: <HardDrive className="w-5 h-5 text-gold-500" />, color: 'gold' },
{ id: 'index', label: 'REINDEX DB', icon: <Database className="w-5 h-5 text-cyan-500" />, color: 'cyan' },
{ id: 'sales', label: 'SALES RT', icon: <ShoppingBag className="w-5 h-5 text-lime-500" />, color: 'lime' },
{ id: 'logs', label: 'SEC LOGS', icon: <Eye className="w-5 h-5 text-white" />, color: 'primary' }
].map((act) => (
<button key={act.id} onClick={() => triggerProtocol(act.label, act.color as any)}
className="flex flex-col items-center justify-center gap-3 h-24 rounded-2xl border border-white/5 bg-white/2 hover:bg-white/5 hover:border-white/20 transition-all group overflow-hidden relative">
<div className={cn("absolute inset-0 bg-gradient-to-br transition-opacity opacity-0 group-hover:opacity-5", `from-${act.color}-500 to-transparent`)} />
<div className="transition-transform group-hover:-translate-y-1">{act.icon}</div>
<span className="text-[10px] font-mono tracking-widest text-muted-foreground group-hover:text-white">{act.label}</span>
</button>
))}
</div>
</Card>
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-[0.3em] mb-6">Node Health</h3>
<div className="space-y-4">
{[{ l: 'CORE_KERNEL', s: 'STABLE', c: 'text-lime-500' },
{ l: 'STORAGE_HIVE', s: '88% CAPACITY', c: 'text-gold-500' },
{ l: 'REST_UPLINK', s: '12ms', c: 'text-cyan-500' },
{ l: 'SECURITY_GRID', s: 'ACTIVE', c: 'text-lime-500' }]
.map((m, i) => (
<div key={i} className="flex justify-between items-center py-2 border-b border-white/5 last:border-0 group">
<span className="text-[10px] font-mono text-muted-foreground group-hover:text-white transition-colors uppercase">{m.l}</span>
<span className={cn("text-[10px] font-bold font-mono tracking-widest", m.c)}>{m.s}</span>
</div>
))}
</div>
</Card>
</div>
</div>
{/* Multi-Tab Interaction Terminal */}
<Tabs defaultValue="reports" className="w-full">
<TabsList className="bg-black/20 border-b border-white/5 w-full justify-start h-auto p-0 rounded-none gap-10 mb-8 backdrop-blur-md">
<TabsTrigger value="reports" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-red-500 data-[state=active]:text-red-500 py-5 px-0 text-lg font-display bg-transparent transition-all">
<div className="flex items-center gap-3">
MODERATION
<span className="bg-red-500/20 text-red-500 px-2 py-0.5 rounded-full text-[10px] font-mono">{reports.length}</span>
</div>
</TabsTrigger>
<TabsTrigger value="uploads" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-5 px-0 text-lg font-display bg-transparent transition-all">
<div className="flex items-center gap-3">
SIGNALS
<span className="bg-primary/20 text-primary px-2 py-0.5 rounded-full text-[10px] font-mono">{uploads.length}</span>
</div>
</TabsTrigger>
<TabsTrigger value="logs" className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-gold-500 data-[state=active]:text-gold-500 py-5 px-0 text-lg font-display bg-transparent transition-all">
<div className="flex items-center gap-3">
SYSTEM LOGS
<History className="w-4 h-4 opacity-50" />
</div>
</TabsTrigger>
</TabsList>
<TabsContent value="reports" className="animate-fadeIn">
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
<div className="divide-y divide-white/5">
{reports.length === 0 ? (
<div className="text-center py-20 text-muted-foreground font-mono uppercase tracking-[0.2em] opacity-40">No pending reports detected.</div>
) : reports.map(r => (
<div key={r.id} className="flex items-center justify-between p-6 hover:bg-white/2 transition-colors group">
<div className="flex items-center gap-5">
<div className="p-3 bg-red-500/10 rounded-xl text-red-500 group-hover:scale-110 transition-transform"><AlertTriangle className="w-5 h-5" /></div>
<div>
<div className="font-bold text-lg text-white group-hover:text-red-400 transition-colors uppercase tracking-tight">{r.targetName}</div>
<div className="text-[10px] text-muted-foreground font-mono uppercase flex items-center gap-2">
<span className="text-red-500/70">{r.targetType}</span> <span className="opacity-60">{r.reason}</span> <span className="opacity-40">{r.timestamp}</span>
</div>
</div>
</div>
<div className="flex gap-3 opacity-0 group-hover:opacity-100 transition-all translate-x-4 group-hover:translate-x-0">
<Button variant="outline" size="sm" className="border-lime-500/20 text-lime-500 hover:bg-lime-500/10" onClick={() => handleAction(r.id, 'cleared')}>
APPROVE
</Button>
<Button variant="outline" size="sm" className="border-red-500/20 text-red-500 hover:bg-red-500/10" onClick={() => handleAction(r.id, 'quarantined')}>
QUARANTINE
</Button>
</div>
</div>
))}
</div>
</Card>
</TabsContent>
<TabsContent value="uploads" className="animate-fadeIn">
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
<div className="divide-y divide-white/5">
{uploads.map(u => (
<div key={u.id} className="flex items-center justify-between p-6 hover:bg-white/2 transition-colors group">
<div className="flex items-center gap-5">
<div className="p-3 bg-primary/10 rounded-xl text-primary"><HardDrive className="w-5 h-5" /></div>
<div>
<div className="font-bold text-lg text-white">{u.name}</div>
<div className="text-[10px] text-muted-foreground font-mono uppercase">User: {u.user} Payload: {u.size} Handshake: {u.date}</div>
</div>
</div>
<Button variant="ghost" size="sm" className="group-hover:bg-primary/10 hover:text-primary">INSPECT</Button>
</div>
))}
</div>
</Card>
</TabsContent>
<TabsContent value="logs" className="animate-fadeIn">
<Card variant="glass" className="bg-black/40 border-white/5 font-mono overflow-hidden">
<div className="bg-black/40 p-4 border-b border-white/5 flex gap-4 text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
<span className="w-32">Timestamp</span>
<span className="w-24">Action</span>
<span className="w-24">Node</span>
<span>Payload Data</span>
</div>
<div className="divide-y divide-white/5 text-[10px]">
{auditLogs.map((log, i) => (
<div key={i} className="flex gap-4 p-4 hover:bg-white/5 transition-colors">
<span className="w-32 text-muted-foreground">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span className="w-24 text-primary font-bold">{log.action || 'AUTH_VAL'}</span>
<span className="w-24 text-magenta-500">USER_{log.user_id?.slice(0, 4)}</span>
<span className="text-white opacity-80 truncate">{JSON.stringify(log.details || log.metadata || {})}</span>
</div>
))}
</div>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export { AdminDashboardView } from './admin-dashboard-view';

View file

@ -0,0 +1,57 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { RefreshCw, Lock, ShieldCheck } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AdminDashboardHeaderProps {
protocolActive: string | null;
onRescan: () => void;
onLockdown: () => void;
}
export function AdminDashboardHeader({
protocolActive,
onRescan,
onLockdown,
}: AdminDashboardHeaderProps) {
return (
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
<div>
<h2 className="text-4xl font-display font-bold text-white mb-2 flex items-center gap-3">
<ShieldCheck className="text-primary w-10 h-10" /> COMMAND CENTER
</h2>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-lime-500 animate-pulse shadow-glow-lime" />
<span className="text-muted-foreground text-xs font-mono tracking-widest uppercase">
Nodes Online
</span>
</div>
<div className="h-3 w-px bg-white/10" />
<span className="text-muted-foreground text-xs font-mono tracking-widest uppercase">
Sector: 00-ALPHA
</span>
</div>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="border-white/10 hover:bg-white/5 font-mono text-xs h-10 tracking-widest"
onClick={onRescan}
>
<RefreshCw
className={cn('w-3 h-3 mr-2', protocolActive === 'RESCAN' && 'animate-spin')}
/>{' '}
FULL RESCAN
</Button>
<Button
variant="outline"
className="border-red-500/20 text-red-500 hover:bg-red-500/10 font-mono text-xs h-10 tracking-widest"
onClick={onLockdown}
>
<Lock className="w-3 h-3 mr-2" /> LOCKDOWN
</Button>
</div>
</div>
);
}

View file

@ -0,0 +1,40 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
const NODES = [
{ l: 'CORE_KERNEL', s: 'STABLE', c: 'text-lime-500' },
{ l: 'STORAGE_HIVE', s: '88% CAPACITY', c: 'text-gold-500' },
{ l: 'REST_UPLINK', s: '12ms', c: 'text-cyan-500' },
{ l: 'SECURITY_GRID', s: 'ACTIVE', c: 'text-lime-500' },
];
export function AdminDashboardNodeHealthCard() {
return (
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-widest mb-6">
Node Health
</h3>
<div className="space-y-4">
{NODES.map((m, i) => (
<div
key={i}
className="flex justify-between items-center py-2 border-b border-white/5 last:border-0 group"
>
<span className="text-xs font-mono text-muted-foreground group-hover:text-white transition-colors uppercase">
{m.l}
</span>
<span
className={cn(
'text-xs font-bold font-mono tracking-widest',
m.c,
)}
>
{m.s}
</span>
</div>
))}
</div>
</Card>
);
}

View file

@ -0,0 +1,73 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { HardDrive, Database, ShoppingBag, Eye } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AdminDashboardProtocolsCardProps {
onTrigger: (name: string, color: string) => void;
}
const PROTOCOLS = [
{
id: 'purge',
label: 'PURGE CACHE',
icon: <HardDrive className="w-5 h-5 text-gold-500" />,
color: 'gold',
},
{
id: 'index',
label: 'REINDEX DB',
icon: <Database className="w-5 h-5 text-cyan-500" />,
color: 'cyan',
},
{
id: 'sales',
label: 'SALES RT',
icon: <ShoppingBag className="w-5 h-5 text-lime-500" />,
color: 'lime',
},
{
id: 'logs',
label: 'SEC LOGS',
icon: <Eye className="w-5 h-5 text-white" />,
color: 'primary',
},
];
export function AdminDashboardProtocolsCard({
onTrigger,
}: AdminDashboardProtocolsCardProps) {
return (
<Card variant="glass" className="p-6 bg-black/40 border-white/5">
<h3 className="text-xs font-mono font-bold text-muted-foreground uppercase tracking-widest mb-6">
Protocols
</h3>
<div className="grid grid-cols-2 gap-4">
{PROTOCOLS.map((act) => (
<button
key={act.id}
type="button"
onClick={() => onTrigger(act.label, act.color)}
className="flex flex-col items-center justify-center gap-3 h-24 rounded-2xl border border-white/5 bg-white/5 hover:bg-white/10 hover:border-white/20 transition-all group overflow-hidden relative"
>
<div
className={cn(
'absolute inset-0 bg-gradient-to-br transition-opacity opacity-0 group-hover:opacity-5 to-transparent',
act.color === 'gold' && 'from-gold-500',
act.color === 'cyan' && 'from-cyan-500',
act.color === 'lime' && 'from-lime-500',
act.color === 'primary' && 'from-primary',
)}
/>
<div className="transition-transform group-hover:-translate-y-1 relative z-10">
{act.icon}
</div>
<span className="text-xs font-mono tracking-widest text-muted-foreground group-hover:text-white relative z-10">
{act.label}
</span>
</button>
))}
</div>
</Card>
);
}

View file

@ -0,0 +1,69 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export function AdminDashboardSkeleton() {
return (
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-layout-content">
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
<div className="space-y-2">
<Skeleton className="h-12 w-80" />
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-28" />
</div>
</div>
<div className="flex gap-3">
<Skeleton className="h-10 w-32" />
<Skeleton className="h-10 w-28" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((i) => (
<Card key={i} variant="glass" className="p-5">
<Skeleton className="h-10 w-10 rounded-xl mb-3" />
<Skeleton className="h-9 w-20 mb-2" />
<Skeleton className="h-4 w-24" />
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<Card variant="glass" className="lg:col-span-2 p-8">
<Skeleton className="h-6 w-32 mb-4" />
<Skeleton className="h-64 w-full rounded-lg" />
</Card>
<div className="space-y-6">
<Card variant="glass" className="p-6">
<Skeleton className="h-4 w-24 mb-6" />
<div className="grid grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-24 rounded-2xl" />
))}
</div>
</Card>
<Card variant="glass" className="p-6">
<Skeleton className="h-4 w-28 mb-6" />
<div className="space-y-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</div>
</Card>
</div>
</div>
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Card variant="glass" className="p-6">
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
</Card>
</div>
</div>
);
}

View file

@ -0,0 +1,69 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import type { StatCardProps } from './types';
const colorClasses: Record<StatCardProps['color'], string> = {
cyan: 'bg-cyan-500',
gold: 'bg-gold-500',
lime: 'bg-lime-500',
red: 'bg-red-500',
};
const textColorClasses: Record<StatCardProps['color'], string> = {
cyan: 'text-cyan-500',
gold: 'text-gold-500',
lime: 'text-lime-500',
red: 'text-red-500',
};
export function AdminDashboardStatCard({
label,
value,
icon,
color,
trend,
}: StatCardProps) {
return (
<Card
variant="glass"
className="p-5 relative overflow-hidden group hover:border-primary/50 transition-all cursor-default"
>
<div
className={cn(
'absolute -right-4 -top-4 w-24 h-24 rounded-full opacity-10 blur-2xl transition-all group-hover:opacity-20',
colorClasses[color],
)}
/>
<div className="flex justify-between items-start mb-3 relative z-10">
<div
className={cn(
'p-2.5 rounded-xl bg-white/5 shadow-inner',
textColorClasses[color],
)}
>
{icon}
</div>
{trend !== undefined && (
<span
className={cn(
'text-xs font-mono font-bold px-2 py-0.5 rounded-full border tracking-tighter',
trend > 0
? 'text-lime-500 border-lime-500/20 bg-lime-500/10'
: 'text-red-500 border-red-500/20 bg-red-500/10',
)}
>
{trend > 0 ? '+' : ''}
{trend}%
</span>
)}
</div>
<div className="text-3xl font-display font-bold text-white mb-1 relative z-10">
{value ?? '—'}
</div>
<div className="text-xs text-muted-foreground uppercase tracking-widest font-mono relative z-10">
{label}
</div>
</Card>
);
}

View file

@ -0,0 +1,181 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { AlertTriangle, HardDrive, History } from 'lucide-react';
import type { Report } from '@/types';
import type { UploadItem, AuditLogItem } from './types';
interface AdminDashboardTabsProps {
reports: Report[];
uploads: UploadItem[];
auditLogs: AuditLogItem[];
onReportAction: (id: string, action: string) => void;
}
export function AdminDashboardTabs({
reports,
uploads,
auditLogs,
onReportAction,
}: AdminDashboardTabsProps) {
return (
<Tabs defaultValue="reports" className="w-full">
<TabsList className="bg-black/20 border-b border-white/5 w-full justify-start h-auto p-0 rounded-none gap-10 mb-8 backdrop-blur-md">
<TabsTrigger
value="reports"
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-red-500 data-[state=active]:text-red-500 py-5 px-0 text-lg font-display bg-transparent transition-all"
>
<div className="flex items-center gap-3">
MODERATION
<span className="bg-red-500/20 text-red-500 px-2 py-0.5 rounded-full text-xs font-mono">
{reports.length}
</span>
</div>
</TabsTrigger>
<TabsTrigger
value="uploads"
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-5 px-0 text-lg font-display bg-transparent transition-all"
>
<div className="flex items-center gap-3">
SIGNALS
<span className="bg-primary/20 text-primary px-2 py-0.5 rounded-full text-xs font-mono">
{uploads.length}
</span>
</div>
</TabsTrigger>
<TabsTrigger
value="logs"
className="relative group rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent data-[state=active]:border-gold-500 data-[state=active]:text-gold-500 py-5 px-0 text-lg font-display bg-transparent transition-all"
>
<div className="flex items-center gap-3">
SYSTEM LOGS
<History className="w-4 h-4 opacity-50" />
</div>
</TabsTrigger>
</TabsList>
<TabsContent value="reports" className="animate-fadeIn">
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
<div className="divide-y divide-white/5">
{reports.length === 0 ? (
<div className="text-center py-20 text-muted-foreground font-mono uppercase tracking-widest opacity-40">
No pending reports detected.
</div>
) : (
reports.map((r) => (
<div
key={r.id}
className="flex items-center justify-between p-6 hover:bg-white/5 transition-colors group"
>
<div className="flex items-center gap-5">
<div className="p-3 bg-red-500/10 rounded-xl text-red-500 group-hover:scale-110 transition-transform">
<AlertTriangle className="w-5 h-5" />
</div>
<div>
<div className="font-bold text-lg text-white group-hover:text-red-400 transition-colors uppercase tracking-tight">
{r.targetName}
</div>
<div className="text-xs text-muted-foreground font-mono uppercase flex items-center gap-2">
<span className="text-red-500/70">{r.targetType}</span> {' '}
<span className="opacity-60">{r.reason}</span> {' '}
<span className="opacity-40">{r.timestamp}</span>
</div>
</div>
</div>
<div className="flex gap-3 opacity-0 group-hover:opacity-100 transition-all translate-x-4 group-hover:translate-x-0">
<Button
variant="outline"
size="sm"
className="border-lime-500/20 text-lime-500 hover:bg-lime-500/10"
onClick={() => onReportAction(r.id, 'cleared')}
>
APPROVE
</Button>
<Button
variant="outline"
size="sm"
className="border-red-500/20 text-red-500 hover:bg-red-500/10"
onClick={() => onReportAction(r.id, 'quarantined')}
>
QUARANTINE
</Button>
</div>
</div>
))
)}
</div>
</Card>
</TabsContent>
<TabsContent value="uploads" className="animate-fadeIn">
<Card variant="glass" className="bg-black/40 border-white/5 overflow-hidden">
<div className="divide-y divide-white/5">
{uploads.map((u) => (
<div
key={u.id}
className="flex items-center justify-between p-6 hover:bg-white/5 transition-colors group"
>
<div className="flex items-center gap-5">
<div className="p-3 bg-primary/10 rounded-xl text-primary">
<HardDrive className="w-5 h-5" />
</div>
<div>
<div className="font-bold text-lg text-white">{u.name}</div>
<div className="text-xs text-muted-foreground font-mono uppercase">
User: {u.user} Payload: {u.size} Handshake: {u.date}
</div>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="group-hover:bg-primary/10 hover:text-primary"
>
INSPECT
</Button>
</div>
))}
</div>
</Card>
</TabsContent>
<TabsContent value="logs" className="animate-fadeIn">
<Card
variant="glass"
className="bg-black/40 border-white/5 font-mono overflow-hidden"
>
<div className="bg-black/40 p-4 border-b border-white/5 flex gap-4 text-xs font-bold text-muted-foreground uppercase tracking-widest">
<span className="w-32">Timestamp</span>
<span className="w-24">Action</span>
<span className="w-24">Node</span>
<span>Payload Data</span>
</div>
<div className="divide-y divide-white/5 text-xs">
{auditLogs.map((log, i) => (
<div
key={i}
className="flex gap-4 p-4 hover:bg-white/5 transition-colors"
>
<span className="w-32 text-muted-foreground">
{log.timestamp
? new Date(log.timestamp).toLocaleTimeString()
: '—'}
</span>
<span className="w-24 text-primary font-bold">
{log.action ?? 'AUTH_VAL'}
</span>
<span className="w-24 text-magenta-500">
USER_{log.user_id != null ? String(log.user_id).slice(0, 4) : '???'}
</span>
<span className="text-white opacity-80 truncate">
{JSON.stringify(log.details ?? log.metadata ?? {})}
</span>
</div>
))}
</div>
</Card>
</TabsContent>
</Tabs>
);
}

View file

@ -0,0 +1,66 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { Server } from 'lucide-react';
export function AdminDashboardTrafficCard() {
return (
<Card
variant="glass"
className="lg:col-span-2 p-8 bg-black/40 border-white/5 relative overflow-hidden"
>
<div className="flex justify-between items-center mb-10 relative z-10">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-3">
<Server className="w-5 h-5 text-primary" /> Traffic Flux
</h3>
<p className="text-xs text-muted-foreground font-mono mt-1">
HOLOGRAPHIC STREAMING INTERFACE
</p>
</div>
<div className="flex gap-8 text-xs font-mono uppercase tracking-widest italic">
<span className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-cyan-500 shadow-glow-cyan" />
Uplink
</span>
<span className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-magenta-500 shadow-glow-magenta" />
Downlink
</span>
</div>
</div>
<div className="h-64 flex items-end gap-0.5 px-2 border-b border-l border-white/10 relative bg-black/30 rounded-lg overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-r from-primary/5 to-transparent w-24 h-full animate-scan pointer-events-none" />
<div className="absolute inset-0 grid grid-rows-4 w-full h-full pointer-events-none">
{[...Array(4)].map((_, i) => (
<div key={i} className="border-t border-white/5 w-full" />
))}
</div>
{Array.from({ length: 60 }).map((_, i) => {
const h1 = Math.random() * 50 + 5;
const h2 = Math.random() * 30 + 10;
return (
<div
key={i}
className="flex-1 flex flex-col justify-end h-full opacity-60 group-hover:opacity-100 transition-opacity"
>
<div
className="w-full bg-magenta-500/50 hover:bg-magenta-500 transition-all rounded-t-sm"
style={{ height: `${h2}%` }}
/>
<div
className="w-full bg-cyan-500/50 hover:bg-cyan-500 transition-all rounded-t-sm"
style={{ height: `${h1}%` }}
/>
</div>
);
})}
</div>
<div className="flex justify-between mt-4 text-xs font-mono text-muted-foreground uppercase opacity-50 px-2 tracking-tighter">
<span>SYS_INIT</span>
<span>BUFFERING_NODES...</span>
<span>LIVE_DATA</span>
</div>
</Card>
);
}

View file

@ -0,0 +1,88 @@
import React from 'react';
import { Users, DollarSign, Activity, ShieldAlert } from 'lucide-react';
import { useAdminDashboardView } from './useAdminDashboardView';
import { AdminDashboardHeader } from './AdminDashboardHeader';
import { AdminDashboardStatCard } from './AdminDashboardStatCard';
import { AdminDashboardTrafficCard } from './AdminDashboardTrafficCard';
import { AdminDashboardProtocolsCard } from './AdminDashboardProtocolsCard';
import { AdminDashboardNodeHealthCard } from './AdminDashboardNodeHealthCard';
import { AdminDashboardTabs } from './AdminDashboardTabs';
import { AdminDashboardSkeleton } from './AdminDashboardSkeleton';
import { Loader2 } from 'lucide-react';
export function AdminDashboardView() {
const {
stats,
reports,
uploads,
auditLogs,
loading,
protocolActive,
handleAction,
triggerProtocol,
} = useAdminDashboardView();
if (loading) {
return (
<div className="flex justify-center py-24">
<Loader2 className="w-10 h-10 text-primary animate-spin" />
</div>
);
}
return (
<div className="space-y-8 pb-24 animate-fadeIn container mx-auto px-4 py-8 max-w-layout-content">
<AdminDashboardHeader
protocolActive={protocolActive}
onRescan={() => triggerProtocol('RESCAN', 'success')}
onLockdown={() => triggerProtocol('LOCKDOWN', 'error')}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<AdminDashboardStatCard
label="Total Nodes"
value={stats.totalUsers?.toLocaleString()}
icon={<Users className="w-5 h-5" />}
trend={stats.trends?.users}
color="cyan"
/>
<AdminDashboardStatCard
label="Credit Volume"
value={`$${stats.monthlyRevenue?.toLocaleString()}`}
icon={<DollarSign className="w-5 h-5" />}
trend={stats.trends?.revenue}
color="gold"
/>
<AdminDashboardStatCard
label="Active Uplinks"
value={stats.activeSessions?.toLocaleString()}
icon={<Activity className="w-5 h-5" />}
trend={stats.trends?.sessions}
color="lime"
/>
<AdminDashboardStatCard
label="Threat Reports"
value={stats.pendingReports}
icon={<ShieldAlert className="w-5 h-5" />}
trend={stats.trends?.reports}
color="red"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<AdminDashboardTrafficCard />
<div className="space-y-6">
<AdminDashboardProtocolsCard onTrigger={triggerProtocol} />
<AdminDashboardNodeHealthCard />
</div>
</div>
<AdminDashboardTabs
reports={reports}
uploads={uploads}
auditLogs={auditLogs}
onReportAction={handleAction}
/>
</div>
);
}

View file

@ -0,0 +1,10 @@
export type {
DashboardStats,
UploadItem,
AuditLogItem,
StatCardProps,
Report,
} from './types';
export { AdminDashboardView } from './AdminDashboardView';
export { AdminDashboardSkeleton } from './AdminDashboardSkeleton';
export { useAdminDashboardView } from './useAdminDashboardView';

View file

@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import type { Report } from '@/types';
export interface DashboardStats {
totalUsers?: number;
monthlyRevenue?: number;
activeSessions?: number;
pendingReports?: number;
trends?: {
users?: number;
revenue?: number;
sessions?: number;
reports?: number;
};
}
export interface UploadItem {
id: string;
name: string;
user: string;
size: string;
date: string;
}
export interface AuditLogItem {
timestamp?: string;
action?: string;
user_id?: string | number;
details?: unknown;
metadata?: unknown;
}
export interface StatCardProps {
label: string;
value: string | number | undefined;
icon: ReactNode;
color: 'cyan' | 'gold' | 'lime' | 'red';
trend?: number;
}
export type { Report };

View file

@ -0,0 +1,72 @@
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@/components/feedback/ToastProvider';
import { adminService } from '@/services/adminService';
import { logger } from '@/utils/logger';
import type { Report } from '@/types';
import type { DashboardStats, UploadItem, AuditLogItem } from './types';
export function useAdminDashboardView() {
const { addToast } = useToast();
const [stats, setStats] = useState<DashboardStats>({});
const [reports, setReports] = useState<Report[]>([]);
const [uploads, setUploads] = useState<UploadItem[]>([]);
const [auditLogs, setAuditLogs] = useState<AuditLogItem[]>([]);
const [loading, setLoading] = useState(true);
const [protocolActive, setProtocolActive] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const [statsData, reportsData, uploadsData, logsData] = await Promise.all([
adminService.getDashboardStats(),
adminService.getModerationQueue('pending'),
adminService.getRecentUploads(),
adminService.getAuditLogs({ limit: 10 }),
]);
setStats(statsData as DashboardStats);
setReports((reportsData as Report[]) || []);
setUploads((uploadsData as UploadItem[]) || []);
setAuditLogs(logsData?.logs || []);
} catch (e) {
logger.error('Error loading admin dashboard', { error: e });
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleAction = useCallback(
async (id: string, action: string) => {
await adminService.resolveReport(id, action);
setReports((prev) => prev.filter((r) => r.id !== id));
addToast(`Protocol "${action.toUpperCase()}" executed successfully.`, 'success');
},
[addToast],
);
const triggerProtocol = useCallback(
(name: string, color: string) => {
setProtocolActive(name);
addToast(`INITIALIZING ${name.toUpperCase()}...`, 'info');
setTimeout(() => {
addToast(`${name.toUpperCase()} DEPLOYED`, color as 'success' | 'error');
setProtocolActive(null);
}, 2000);
},
[addToast],
);
return {
stats,
reports,
uploads,
auditLogs,
loading,
protocolActive,
handleAction,
triggerProtocol,
};
}