Comprehensive UX polish: fix 19 friction points across all pages
CI / build-and-push (push) Successful in 33s
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:
@@ -1,5 +1,6 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield, X } from 'lucide-react';
|
||||
import { TutorialHint } from '@/components/game/TutorialHint';
|
||||
import { useGameStore } from '@/store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
import type { DCTier, RackSkuId, LocationId, RackOrder, PipelineStage, Era } from '@ai-tycoon/shared';
|
||||
|
||||
const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
const collapsedDCs = new Set<string>();
|
||||
|
||||
const STAGE_LABELS: Record<PipelineStage, string> = {
|
||||
ordered: 'Ordered',
|
||||
@@ -113,7 +115,17 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
||||
const orderRack = useGameStore((s) => s.orderRack);
|
||||
const decommissionRack = useGameStore((s) => s.decommissionRack);
|
||||
const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter);
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [expanded, setExpanded] = useState(!collapsedDCs.has(dcId));
|
||||
const [confirmDecom, setConfirmDecom] = useState<string | null>(null);
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setExpanded(prev => {
|
||||
const next = !prev;
|
||||
if (next) collapsedDCs.delete(dcId);
|
||||
else collapsedDCs.add(dcId);
|
||||
return next;
|
||||
});
|
||||
}, [dcId]);
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const currentEraIdx = ERA_ORDER.indexOf(era);
|
||||
@@ -166,7 +178,7 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
||||
<span className="text-danger">Cost: {formatMoney(dc.energyCostPerTick + dc.maintenanceCostPerTick)}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setExpanded(!expanded)} className="text-surface-400 hover:text-surface-200">
|
||||
<button onClick={toggleExpanded} className="text-surface-400 hover:text-surface-200">
|
||||
{expanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
@@ -190,15 +202,37 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
||||
? 'bg-surface-800 border-surface-600'
|
||||
: 'bg-danger/10 border-danger/30'
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => decommissionRack(dc.id, rack.id)}
|
||||
className="absolute top-0.5 right-0.5 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-danger/20 text-surface-400 hover:text-danger transition-all"
|
||||
title="Decommission rack"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
<div className="font-medium truncate">{sku.name}</div>
|
||||
<div className="text-surface-400">{formatNumber(sku.flopsPerRack)} FLOPS</div>
|
||||
{confirmDecom === rack.id ? (
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="text-danger text-[10px]">Remove?</span>
|
||||
<div className="flex gap-0.5">
|
||||
<button
|
||||
onClick={() => { decommissionRack(dc.id, rack.id); setConfirmDecom(null); }}
|
||||
className="px-1 py-0.5 rounded bg-danger/20 text-danger hover:bg-danger/30 text-[10px]"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDecom(null)}
|
||||
className="px-1 py-0.5 rounded hover:bg-surface-700 text-surface-400 text-[10px]"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setConfirmDecom(rack.id)}
|
||||
className="absolute top-0.5 right-0.5 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-danger/20 text-surface-400 hover:text-danger transition-all"
|
||||
title="Decommission rack"
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
<div className="font-medium truncate">{sku.name}</div>
|
||||
<div className="text-surface-400">{formatNumber(sku.flopsPerRack)} FLOPS</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -214,6 +248,7 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
||||
const hasSlot = liveUsedSlots < tierConfig.rackSlots;
|
||||
const hasPower = liveUsedPower + sku.powerDrawKW <= tierConfig.powerBudgetKW;
|
||||
const disabled = !canAfford || !hasSlot || !hasPower;
|
||||
const reason = !canAfford ? `Need ${formatMoney(sku.baseCost)}` : !hasSlot ? 'No slots available' : !hasPower ? 'Exceeds power budget' : '';
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -221,10 +256,10 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
||||
onClick={() => orderRack(dc.id, sku.id)}
|
||||
disabled={disabled}
|
||||
className="bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded-lg px-2.5 py-1.5 text-xs disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-left"
|
||||
title={!hasSlot ? 'No slots available' : !hasPower ? 'Exceeds power budget' : ''}
|
||||
>
|
||||
<div className="font-medium">{sku.name}</div>
|
||||
<div className="text-surface-400">{formatNumber(sku.flopsPerRack)} FLOPS · {sku.powerDrawKW}kW · {formatMoney(sku.baseCost)}</div>
|
||||
{disabled && reason && <div className="text-warning mt-0.5">{reason}</div>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -364,6 +399,10 @@ export function InfrastructurePage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TutorialHint id="infra-intro">
|
||||
Choose a location and tier for your data center, then order GPU racks to add compute capacity. Racks go through a build pipeline before they come online.
|
||||
</TutorialHint>
|
||||
|
||||
{showNewDC && <BuildDCPanel onClose={() => setShowNewDC(false)} />}
|
||||
|
||||
<PipelineKanban />
|
||||
|
||||
Reference in New Issue
Block a user