forked from syntaxbullet/aurorabot
Replace placeholder panel with a full dashboard landing page showing bot stats, leaderboards, and recent events from /api/stats. Add sidebar navigation with placeholder pages for Users, Items, Classes, Quests, Lootdrops, Moderation, Transactions, and Settings. Update theme to match Aurora design guidelines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
298 lines
8.7 KiB
TypeScript
298 lines
8.7 KiB
TypeScript
import {
|
|
Users,
|
|
Coins,
|
|
TrendingUp,
|
|
Gift,
|
|
Loader2,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Info,
|
|
Clock,
|
|
Wifi,
|
|
Trophy,
|
|
Crown,
|
|
Gem,
|
|
Wrench,
|
|
} from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
import { useDashboard, type DashboardStats } from "../lib/useDashboard";
|
|
|
|
function formatNumber(n: number | string): string {
|
|
const num = typeof n === "string" ? parseFloat(n) : n;
|
|
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
|
|
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
|
|
return num.toLocaleString();
|
|
}
|
|
|
|
function formatUptime(ms: number): string {
|
|
const hours = Math.floor(ms / 3_600_000);
|
|
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
|
if (hours >= 24) {
|
|
const days = Math.floor(hours / 24);
|
|
return `${days}d ${hours % 24}h`;
|
|
}
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
|
|
function timeAgo(ts: string | Date): string {
|
|
const diff = Date.now() - new Date(ts).getTime();
|
|
const mins = Math.floor(diff / 60_000);
|
|
if (mins < 1) return "just now";
|
|
if (mins < 60) return `${mins}m ago`;
|
|
const hours = Math.floor(mins / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
return `${Math.floor(hours / 24)}d ago`;
|
|
}
|
|
|
|
const eventIcons = {
|
|
success: CheckCircle,
|
|
error: XCircle,
|
|
warn: AlertTriangle,
|
|
info: Info,
|
|
} as const;
|
|
|
|
const eventColors = {
|
|
success: "text-success",
|
|
error: "text-destructive",
|
|
warn: "text-warning",
|
|
info: "text-info",
|
|
} as const;
|
|
|
|
function StatCard({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
sub,
|
|
accent = "border-primary",
|
|
}: {
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
label: string;
|
|
value: string;
|
|
sub?: string;
|
|
accent?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-gradient-to-br from-card to-surface rounded-lg border border-border p-6 border-l-4",
|
|
accent
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Icon className="w-5 h-5 text-primary" />
|
|
<span className="text-[11px] font-semibold uppercase tracking-wider text-text-tertiary">
|
|
{label}
|
|
</span>
|
|
</div>
|
|
<div className="text-3xl font-bold font-display tracking-tight">{value}</div>
|
|
{sub && <div className="text-sm text-text-tertiary mt-1">{sub}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LeaderboardColumn({
|
|
title,
|
|
icon: Icon,
|
|
entries,
|
|
valueKey,
|
|
valuePrefix,
|
|
}: {
|
|
title: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
entries: Array<{ username: string; [k: string]: unknown }>;
|
|
valueKey: string;
|
|
valuePrefix?: string;
|
|
}) {
|
|
return (
|
|
<div className="bg-card rounded-lg border border-border">
|
|
<div className="flex items-center gap-2 px-5 py-4 border-b border-border">
|
|
<Icon className="w-4 h-4 text-primary" />
|
|
<h3 className="text-sm font-semibold">{title}</h3>
|
|
</div>
|
|
<div className="divide-y divide-border">
|
|
{entries.length === 0 && (
|
|
<div className="px-5 py-4 text-sm text-text-tertiary">No data</div>
|
|
)}
|
|
{entries.slice(0, 10).map((entry, i) => (
|
|
<div
|
|
key={entry.username}
|
|
className="flex items-center justify-between px-5 py-3 hover:bg-raised/40 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span
|
|
className={cn(
|
|
"w-6 text-xs font-mono font-medium text-right",
|
|
i === 0
|
|
? "text-gold"
|
|
: i === 1
|
|
? "text-text-secondary"
|
|
: i === 2
|
|
? "text-warning"
|
|
: "text-text-tertiary"
|
|
)}
|
|
>
|
|
#{i + 1}
|
|
</span>
|
|
<span className="text-sm">{entry.username}</span>
|
|
</div>
|
|
<span className="text-sm font-mono text-text-secondary">
|
|
{valuePrefix}
|
|
{formatNumber(entry[valueKey] as string | number)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const { data, loading, error } = useDashboard();
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-32">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center py-32">
|
|
<div className="text-center">
|
|
<AlertTriangle className="w-8 h-8 text-warning mx-auto mb-3" />
|
|
<p className="text-sm text-text-tertiary">{error}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!data) return null;
|
|
|
|
return <DashboardContent data={data} />;
|
|
}
|
|
|
|
function DashboardContent({ data }: { data: DashboardStats }) {
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Maintenance banner */}
|
|
{data.maintenanceMode && (
|
|
<div className="flex items-center gap-3 bg-warning/10 border border-warning/30 rounded-lg px-5 py-3">
|
|
<Wrench className="w-4 h-4 text-warning shrink-0" />
|
|
<span className="text-sm text-warning font-medium">
|
|
Maintenance mode is active
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stat cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard
|
|
icon={Users}
|
|
label="Total Users"
|
|
value={formatNumber(data.users.total)}
|
|
sub={`${formatNumber(data.users.active)} active`}
|
|
accent="border-info"
|
|
/>
|
|
<StatCard
|
|
icon={Coins}
|
|
label="Total Wealth"
|
|
value={formatNumber(data.economy.totalWealth)}
|
|
accent="border-gold"
|
|
/>
|
|
<StatCard
|
|
icon={TrendingUp}
|
|
label="Avg Level"
|
|
value={data.economy.avgLevel.toFixed(1)}
|
|
sub={`Top streak: ${data.economy.topStreak}`}
|
|
accent="border-success"
|
|
/>
|
|
<StatCard
|
|
icon={Gift}
|
|
label="Active Lootdrops"
|
|
value={String(data.activeLootdrops?.length ?? 0)}
|
|
accent="border-primary"
|
|
/>
|
|
</div>
|
|
|
|
{/* Leaderboards */}
|
|
{data.leaderboards && (
|
|
<section>
|
|
<h2 className="font-display text-lg font-semibold mb-4">Leaderboards</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<LeaderboardColumn
|
|
title="Top Levels"
|
|
icon={Trophy}
|
|
entries={data.leaderboards.topLevels}
|
|
valueKey="level"
|
|
valuePrefix="Lv. "
|
|
/>
|
|
<LeaderboardColumn
|
|
title="Top Wealth"
|
|
icon={Crown}
|
|
entries={data.leaderboards.topWealth}
|
|
valueKey="balance"
|
|
/>
|
|
<LeaderboardColumn
|
|
title="Top Net Worth"
|
|
icon={Gem}
|
|
entries={data.leaderboards.topNetWorth}
|
|
valueKey="netWorth"
|
|
/>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Recent Events */}
|
|
{data.recentEvents.length > 0 && (
|
|
<section>
|
|
<h2 className="font-display text-lg font-semibold mb-4">Recent Events</h2>
|
|
<div className="bg-card rounded-lg border border-border divide-y divide-border">
|
|
{data.recentEvents.slice(0, 20).map((event, i) => {
|
|
const Icon = eventIcons[event.type];
|
|
return (
|
|
<div
|
|
key={i}
|
|
className="flex items-start gap-3 px-5 py-3 hover:bg-raised/40 transition-colors"
|
|
>
|
|
<Icon
|
|
className={cn("w-4 h-4 mt-0.5 shrink-0", eventColors[event.type])}
|
|
/>
|
|
<span className="text-sm flex-1">
|
|
{event.icon && <span className="mr-1.5">{event.icon}</span>}
|
|
{event.message}
|
|
</span>
|
|
<span className="text-xs text-text-tertiary font-mono whitespace-nowrap">
|
|
{timeAgo(event.timestamp)}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Bot status footer */}
|
|
<footer className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-text-tertiary border-t border-border pt-6">
|
|
<span className="font-medium text-text-secondary">{data.bot.name}</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<Wifi className="w-3 h-3" />
|
|
{Math.round(data.ping.avg)}ms
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<Clock className="w-3 h-3" />
|
|
{formatUptime(data.uptime)}
|
|
</span>
|
|
{data.bot.status && (
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="w-2 h-2 rounded-full bg-success" />
|
|
{data.bot.status}
|
|
</span>
|
|
)}
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|