Files
AIHostingTycoon/apps/web/src/pages/LeaderboardPage.tsx
T
josh 0ff8a32b95 Add Week 4 social features, regulation, and safety tradeoffs
Leaderboard page with category tabs and score submission, shareable
company stats card with clipboard copy, dynamic regulation system
(compliance costs scale with capability and era, regulatory standing
tracks safety research), 6 geopolitical events (export controls, energy
crisis, natural disaster, AI safety summit, immigration policy, data
sovereignty), safety-capability tradeoff (safety score affects benchmark,
low safety triggers incidents with reputation damage), and enhanced
event consequence handling for regulation and talent types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 18:02:30 -04:00

125 lines
4.6 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Trophy, Medal, Clock, TrendingUp } from 'lucide-react';
import { useGameStore } from '@/store';
import { formatMoney, formatNumber } from '@ai-tycoon/shared';
import { api, getAuthToken } from '@/lib/api';
interface LeaderboardEntry {
companyName: string;
score: number;
era: string;
tickCount: number;
}
const CATEGORIES = [
{ id: 'revenue', label: 'Highest Revenue', icon: TrendingUp },
{ id: 'fastest-agi', label: 'Fastest to AGI', icon: Clock },
{ id: 'capability', label: 'Best Model', icon: Trophy },
] as const;
export function LeaderboardPage() {
const [category, setCategory] = useState('revenue');
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const companyName = useGameStore((s) => s.meta.companyName);
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
const era = useGameStore((s) => s.meta.currentEra);
const tickCount = useGameStore((s) => s.meta.tickCount);
const bestModel = useGameStore((s) =>
s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0),
);
useEffect(() => {
setLoading(true);
api.leaderboard.get(category)
.then(data => setEntries(data.entries))
.catch(() => setEntries([]))
.finally(() => setLoading(false));
}, [category]);
const handleSubmit = async () => {
if (!getAuthToken()) return;
const scoreMap: Record<string, number> = {
revenue: Math.floor(totalRevenue),
'fastest-agi': era === 'agi' ? tickCount : 0,
capability: Math.floor(bestModel * 10),
};
const score = scoreMap[category];
if (score <= 0) return;
try {
await api.leaderboard.submit({ companyName, category, score, era, tickCount });
setSubmitted(true);
const data = await api.leaderboard.get(category);
setEntries(data.entries);
} catch { /* ignore */ }
};
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Leaderboard</h2>
<div className="flex gap-2">
{CATEGORIES.map(cat => (
<button
key={cat.id}
onClick={() => { setCategory(cat.id); setSubmitted(false); }}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm border transition-colors ${
category === cat.id
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-900 border-surface-700 text-surface-300 hover:border-surface-500'
}`}
>
<cat.icon size={14} />
{cat.label}
</button>
))}
</div>
{getAuthToken() && !submitted && (
<button
onClick={handleSubmit}
className="bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg text-sm font-medium"
>
Submit My Score
</button>
)}
{submitted && <p className="text-sm text-success">Score submitted!</p>}
<div className="bg-surface-900 border border-surface-700 rounded-xl overflow-hidden">
{loading ? (
<div className="p-8 text-center text-surface-500">Loading...</div>
) : entries.length === 0 ? (
<div className="p-8 text-center text-surface-500">
<Trophy size={48} className="mx-auto mb-4 opacity-50" />
<p>No entries yet. Be the first!</p>
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-700 text-surface-400 text-xs uppercase">
<th className="p-3 text-left w-12">#</th>
<th className="p-3 text-left">Company</th>
<th className="p-3 text-right">Score</th>
<th className="p-3 text-right">Era</th>
</tr>
</thead>
<tbody>
{entries.map((entry, i) => (
<tr key={i} className="border-b border-surface-800 hover:bg-surface-800/50">
<td className="p-3">
{i < 3 ? <Medal size={16} className={i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-400' : 'text-orange-400'} /> : i + 1}
</td>
<td className="p-3 font-medium">{entry.companyName}</td>
<td className="p-3 text-right font-mono">{formatNumber(entry.score)}</td>
<td className="p-3 text-right text-surface-400 capitalize">{entry.era}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}