client: screens and app routing

Add all game screens: Login, Dashboard, Market (buy/sell), Inventory,
Missions, Travel (city selection), Leaderboard; wired via react-router
in App.tsx with protected routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:42:35 +02:00
co-authored by Claude Sonnet 4.6
parent 95864ce392
commit 0f91ace43e
8 changed files with 736 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../api/api';
import { ApiError } from '../api/http';
import type { City } from '../api/types';
import { useAuth } from '../auth/AuthContext';
import { useToast } from '../components/Toast';
import { formatMoney, riskLabel } from '../shared/format';
export function TravelScreen() {
const { player, setPlayer } = useAuth();
const { toast } = useToast();
const [cities, setCities] = useState<City[]>([]);
const [busy, setBusy] = useState(false);
const reload = useCallback(async () => {
const res = await api.cities();
setCities(res.cities);
}, []);
useEffect(() => {
void reload();
}, [reload]);
async function travelTo(city: City) {
setBusy(true);
try {
const res = await api.travel(city.id);
setPlayer(res.player);
toast(`Benvenuto a ${res.cityName} (${formatMoney(res.cost)})`, 'success');
} catch (err) {
toast(err instanceof ApiError ? err.message : 'Errore imprevisto', 'error');
} finally {
setBusy(false);
}
}
return (
<div className="screen">
<h2 className="screen__title">Città</h2>
<ul className="list">
{cities.map((city) => {
const isCurrent = player?.currentCityId === city.id;
const canAfford = (player?.money ?? 0) >= city.travelCost;
return (
<li key={city.id} className={`list__row${isCurrent ? ' is-current' : ''}`}>
<div>
<strong>{city.name}</strong>
{isCurrent && <span className="badge badge--small">sei qui</span>}
<p className="muted small">
rischio {riskLabel(city.riskLevel)} · economia ×{city.economyModifier}
</p>
</div>
{!isCurrent && (
<button
type="button"
className="btn btn--secondary btn--small"
disabled={busy || !canAfford}
onClick={() => void travelTo(city)}
>
Viaggia · {formatMoney(city.travelCost)}
</button>
)}
</li>
);
})}
</ul>
</div>
);
}