Comprehensive UX polish: fix 19 friction points across all pages
CI / build-and-push (push) Successful in 33s

Addresses broken interactions (notification bell, browser dialogs),
missing feedback states (disabled buttons, pricing changes, paused
indicator), unclear affordances (research queue, model tuning, funding
requirements), and navigation gaps (hash routing, keyboard shortcuts,
clickable dashboard cards, sidebar grouping, tutorial hints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:44:18 -04:00
parent d25dfe0435
commit f9f6233b69
19 changed files with 540 additions and 121 deletions
+52 -22
View File
@@ -1,8 +1,8 @@
import { useGameStore } from '@/store';
import { formatMoney, formatPercent, FUNDING_ROUNDS } from '@ai-tycoon/shared';
import { formatMoney, formatPercent, formatNumber, FUNDING_ROUNDS } from '@ai-tycoon/shared';
import type { FundingRoundType } from '@ai-tycoon/shared';
import { TrendingUp, DollarSign, PiggyBank, BarChart3, Rocket } from 'lucide-react';
import { AreaChart, Area, XAxis, YAxis, ResponsiveContainer, LineChart, Line } from 'recharts';
import { TrendingUp, DollarSign, PiggyBank, BarChart3, Rocket, Check, X as XIcon } from 'lucide-react';
import { AreaChart, Area, XAxis, YAxis, ResponsiveContainer, LineChart, Line, Tooltip } from 'recharts';
import { canRaiseFunding } from '@ai-tycoon/game-engine';
import type { GameState } from '@ai-tycoon/shared';
@@ -15,6 +15,9 @@ export function FinancePage() {
const infrastructure = useGameStore((s) => s.infrastructure);
const talent = useGameStore((s) => s.talent);
const raiseFunding = useGameStore((s) => s.raiseFunding);
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
const reputationScore = useGameStore((s) => s.reputation.score);
const state = useGameStore.getState();
const gameStateForFunding: GameState = {
@@ -81,12 +84,22 @@ export function FinancePage() {
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-surface-400 mb-4">Revenue vs Expenses</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-surface-400">Revenue vs Expenses</h3>
<div className="flex items-center gap-4 text-xs">
<span className="flex items-center gap-1"><span className="w-2.5 h-2.5 rounded-full bg-success inline-block" />Revenue</span>
<span className="flex items-center gap-1"><span className="w-2.5 h-2.5 rounded-full bg-danger inline-block" />Expenses</span>
</div>
</div>
{history.length > 1 ? (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={history}>
<XAxis dataKey="tick" hide />
<YAxis hide />
<Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
formatter={(value: number, name: string) => [formatMoney(value), name === 'revenue' ? 'Revenue' : 'Expenses']}
/>
<Line type="monotone" dataKey="revenue" stroke="#22c55e" dot={false} strokeWidth={2} />
<Line type="monotone" dataKey="expenses" stroke="#ef4444" dot={false} strokeWidth={2} />
</LineChart>
@@ -143,26 +156,43 @@ export function FinancePage() {
)}
</div>
{fundingStatus.nextRound && (
<div className="bg-surface-800 rounded-lg p-4 mb-4 border border-surface-600">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-sm capitalize">
{fundingStatus.nextRound === 'ipo' ? 'IPO' : fundingStatus.nextRound.replace('series', 'Series ')}
</h4>
{fundingStatus.canRaise ? (
<button
onClick={() => raiseFunding(fundingStatus.nextRound!)}
className="flex items-center gap-1.5 bg-accent hover:bg-accent-dark text-white rounded-lg px-4 py-2 text-sm font-medium"
>
<Rocket size={14} />
Raise {formatMoney(FUNDING_ROUNDS[fundingStatus.nextRound! as FundingRoundType].amount)}
</button>
) : (
<span className="text-xs text-warning">{String(fundingStatus.reason ?? '')}</span>
{fundingStatus.nextRound && (() => {
const roundConfig = FUNDING_ROUNDS[fundingStatus.nextRound as FundingRoundType];
const reqs = roundConfig.requirements;
const checks = [
...(reqs.minRevenue ? [{ label: `Total Revenue: ${formatMoney(totalRevenue)} / ${formatMoney(reqs.minRevenue)}`, met: totalRevenue >= reqs.minRevenue }] : []),
...(reqs.minUsers ? [{ label: `Subscribers: ${formatNumber(subscribers)} / ${formatNumber(reqs.minUsers)}`, met: subscribers >= reqs.minUsers }] : []),
...(reqs.minReputation ? [{ label: `Reputation: ${reputationScore} / ${reqs.minReputation}`, met: reputationScore >= reqs.minReputation }] : []),
];
return (
<div className="bg-surface-800 rounded-lg p-4 mb-4 border border-surface-600">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-sm capitalize">
{fundingStatus.nextRound === 'ipo' ? 'IPO' : fundingStatus.nextRound.replace('series', 'Series ')}
</h4>
{fundingStatus.canRaise && (
<button
onClick={() => raiseFunding(fundingStatus.nextRound!)}
className="flex items-center gap-1.5 bg-accent hover:bg-accent-dark text-white rounded-lg px-4 py-2 text-sm font-medium"
>
<Rocket size={14} />
Raise {formatMoney(roundConfig.amount)}
</button>
)}
</div>
{checks.length > 0 && (
<div className="space-y-1.5 mt-2">
{checks.map((c, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
{c.met ? <Check size={12} className="text-success shrink-0" /> : <XIcon size={12} className="text-danger shrink-0" />}
<span className={c.met ? 'text-surface-400' : 'text-surface-200'}>{c.label}</span>
</div>
))}
</div>
)}
</div>
</div>
)}
);
})()}
{funding.completedRounds.length === 0 ? (
<p className="text-sm text-surface-500">No funding rounds completed yet.</p>