Files
AIHostingTycoon/apps/web/src/pages/CompetitorsPage.tsx
T
josh 8d650fefae
CI / build-and-push (push) Successful in 28s
Comprehensive UX audit fixes: navigation, feedback, affordances, and accessibility
Address 18 issues across high/medium/low impact tiers identified in a full
interface review. Key changes: Models page decomposed into tabs, confirmation
dialogs for irreversible actions (deploy/open-source/acquire), chart Y-axes
made visible, hash router extended for Market tab persistence, collapsible
sidebar, keyboard navigation shortcuts (g+key chords), notification bulk
actions, achievement progress bars, and ARIA label improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 09:05:26 -04:00

178 lines
7.9 KiB
TypeScript

import { useState } from 'react';
import { Swords, TrendingUp, Shield, Users, Brain, ShoppingCart } from 'lucide-react';
import { useGameStore } from '@/store';
import { ConfirmModal } from '@/components/common/ConfirmModal';
import { Tooltip } from '@/components/common/Tooltip';
import { formatMoney, formatNumber } from '@ai-tycoon/shared';
import type { Era } from '@ai-tycoon/shared';
const ARCHETYPE_LABELS: Record<string, string> = {
'safety-first': 'Safety-First Lab',
'move-fast': 'Move-Fast Startup',
'big-tech': 'Big Tech Giant',
'open-source': 'Open Source Maximalist',
'stealth-startup': 'Stealth Startup',
};
const ARCHETYPE_COLORS: Record<string, string> = {
'safety-first': 'text-green-400',
'move-fast': 'text-red-400',
'big-tech': 'text-blue-400',
'open-source': 'text-orange-400',
'stealth-startup': 'text-purple-400',
};
export function CompetitorsPage() {
const rivals = useGameStore((s) => s.competitors.rivals);
const industryBenchmark = useGameStore((s) => s.competitors.industryBenchmark);
const playerBest = useGameStore((s) => s.models.bestDeployedModelScore);
const era = useGameStore((s) => s.meta.currentEra);
const money = useGameStore((s) => s.economy.money);
const acquireCompetitor = useGameStore((s) => s.acquireCompetitor);
const canAcquire = (era: Era) => era === 'bigtech' || era === 'agi';
const [acquireConfirm, setAcquireConfirm] = useState<{ id: string; name: string; cost: number } | null>(null);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Competitors</h2>
<div className="text-sm text-surface-400">
Industry Benchmark: <span className="text-surface-100 font-mono">{industryBenchmark.toFixed(1)}</span>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-surface-400 mb-3">Your Position</h3>
<div className="flex items-center gap-6">
<div>
<div className="text-xs text-surface-500">Best Model</div>
<div className="text-lg font-mono font-bold">{playerBest.toFixed(1)}/100</div>
</div>
<div className="flex-1">
<div className="h-3 bg-surface-800 rounded-full overflow-hidden relative">
<div
className="absolute h-full bg-accent rounded-full transition-all"
style={{ width: `${playerBest}%` }}
/>
{rivals.filter(r => r.status === 'active').map(rival => (
<div
key={rival.id}
className="absolute top-0 h-full w-0.5 bg-danger"
style={{ left: `${rival.estimatedCapability}%` }}
/>
))}
</div>
<div className="flex items-center gap-4 mt-1.5 text-[10px]">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-accent inline-block" />You: {playerBest.toFixed(1)}</span>
{rivals.filter(r => r.status === 'active').map(rival => (
<span key={rival.id} className="flex items-center gap-1"><span className="w-2 h-0.5 bg-danger inline-block" />{rival.name}: {rival.estimatedCapability.toFixed(1)}</span>
))}
</div>
</div>
</div>
</div>
<div className="space-y-4">
{rivals.map(rival => (
<div
key={rival.id}
className={`bg-surface-900 border rounded-xl p-4 ${
rival.status === 'active' ? 'border-surface-700' : 'border-surface-800 opacity-50'
}`}
>
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-semibold text-lg">{rival.name}</h3>
<span className={`text-xs ${ARCHETYPE_COLORS[rival.archetype]}`}>
{ARCHETYPE_LABELS[rival.archetype]}
</span>
</div>
<div className="flex items-center gap-2">
{canAcquire(era) && rival.status === 'active' && (() => {
const cost = rival.estimatedRevenue * 500 + rival.estimatedCapability * 100_000;
return (
<div className="text-right">
<button
onClick={() => setAcquireConfirm({ id: rival.id, name: rival.name, cost })}
disabled={money < cost}
className="flex items-center gap-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/30 rounded px-3 py-1.5 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
<ShoppingCart size={12} />
Acquire ({formatMoney(cost)})
</button>
{money < cost && (
<div className="text-[10px] text-warning mt-0.5">Need {formatMoney(cost - money)} more</div>
)}
</div>
);
})()}
<span className={`text-xs px-2 py-1 rounded-full ${
rival.status === 'active' ? 'bg-success/20 text-success' :
rival.status === 'acquired' ? 'bg-blue-500/20 text-blue-400' :
'bg-surface-700 text-surface-400'
}`}>
{rival.status}
</span>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<Stat icon={Brain} label="Capability" value={rival.estimatedCapability.toFixed(1)} sub={rival.latestModelName} />
<Stat icon={TrendingUp} label="Est. Revenue" value={formatMoney(rival.estimatedRevenue)} sub="/tick" />
<Stat icon={Users} label="Est. Users" value={formatNumber(rival.estimatedUsers)} />
<Stat icon={Shield} label="Reputation" value={`${rival.reputation}/100`} />
</div>
<div className="mt-3 grid grid-cols-6 gap-1">
{Object.entries(rival.personality).map(([key, val]) => (
<div key={key} className="text-center">
<div className="h-1 bg-surface-800 rounded-full overflow-hidden mb-1">
<div className="h-full bg-accent rounded-full" style={{ width: `${(val as number) * 100}%` }} />
</div>
<div className="text-[10px] text-surface-500 capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</div>
</div>
))}
</div>
</div>
))}
</div>
{rivals.length === 0 && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500">
<Swords size={48} className="mx-auto mb-4 opacity-50" />
<p>No competitors detected yet.</p>
</div>
)}
{acquireConfirm && (
<ConfirmModal
title="Acquire Competitor"
message={`Acquire "${acquireConfirm.name}" for ${formatMoney(acquireConfirm.cost)}? This will absorb their users and technology. This cannot be undone.`}
confirmLabel={`Acquire (${formatMoney(acquireConfirm.cost)})`}
danger
onConfirm={() => { acquireCompetitor(acquireConfirm.id); setAcquireConfirm(null); }}
onCancel={() => setAcquireConfirm(null)}
/>
)}
</div>
);
}
function Stat({ icon: Icon, label, value, sub }: {
icon: typeof Brain;
label: string;
value: string;
sub?: string;
}) {
return (
<div className="bg-surface-800 rounded-lg p-2">
<div className="flex items-center gap-1 mb-1">
<Icon size={12} className="text-surface-400" />
<span className="text-xs text-surface-400">{label}</span>
</div>
<div className="text-sm font-mono font-semibold">{value}</div>
{sub && <div className="text-[10px] text-surface-500">{sub}</div>}
</div>
);
}