client: dashboard — active missions widget and quicklinks

Home screen now loads active missions alongside player/events data.
Shows a "Missioni in corso" card with per-mission countdown and
inline Riscuoti button (with toast feedback). Quick-access buttons
to Missioni, Mercato, Classifiche replace the single leaderboard link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:12:08 +02:00
parent bdee84244f
commit 18de7fda21
+85 -10
View File
@@ -1,27 +1,65 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../api/api';
import type { PlayerMeResponse, WorldEvent } from '../api/types';
import { ApiError } from '../api/http';
import type { ActiveMission, PlayerMeResponse, WorldEvent } from '../api/types';
import { useAuth } from '../auth/AuthContext';
import { Countdown } from '../components/Countdown';
import { useToast } from '../components/Toast';
import { EVENT_TYPE_LABELS, formatMoney, riskLabel } from '../shared/format';
export function DashboardScreen() {
const { player, setPlayer, logout } = useAuth();
const { toast } = useToast();
const [me, setMe] = useState<PlayerMeResponse | null>(null);
const [events, setEvents] = useState<WorldEvent[]>([]);
const [active, setActive] = useState<ActiveMission[]>([]);
const [busy, setBusy] = useState(false);
// tick fittizio per ri-renderizzare quando un countdown arriva a zero
const [, setTick] = useState(0);
const reload = useCallback(async () => {
const [meRes, eventsRes, activeRes] = await Promise.all([
api.playerMe(),
api.events(),
api.missionsActive(),
]);
setMe(meRes);
setPlayer(meRes.player);
setEvents(eventsRes.events);
setActive(activeRes.missions);
}, [setPlayer]);
useEffect(() => {
void api.playerMe().then((res) => {
setMe(res);
void reload();
}, [reload]);
async function quickClaim(playerMissionId: string) {
setBusy(true);
try {
const res = await api.claimMission(playerMissionId);
setPlayer(res.player);
});
void api.events().then((res) => setEvents(res.events));
}, [setPlayer]);
toast(
res.success
? `✅ "${res.missionTitle}": +${formatMoney(res.rewardMoney)}, +${res.rewardXp} XP` +
(res.lootItemName ? `, 🎁 ${res.lootItemQuantity}× ${res.lootItemName}` : '')
: `❌ "${res.missionTitle}" fallita` +
(res.fine > 0 ? `: multa ${formatMoney(res.fine)}` : ''),
res.success ? 'success' : 'error',
);
await reload();
} catch (err) {
toast(err instanceof ApiError ? err.message : 'Errore imprevisto', 'error');
} finally {
setBusy(false);
}
}
if (!player) return null;
const xpNext = me?.xpForNextLevel ?? null;
const xpRatio = xpNext ? Math.min(1, player.experience / xpNext) : 0;
const isReady = (m: ActiveMission) => new Date(m.completesAt).getTime() <= Date.now();
return (
<div className="screen">
@@ -55,6 +93,46 @@ export function DashboardScreen() {
)}
</section>
{active.length > 0 && (
<section className="card">
<h3 className="card__title">Missioni in corso</h3>
<ul className="list">
{active.map((m) => (
<li key={m.playerMissionId} className="list__row">
<div>
<strong>{m.mission.title}</strong>
<p className="muted small">{formatMoney(m.mission.rewardMoney)}</p>
</div>
{isReady(m) ? (
<button
type="button"
className="btn btn--primary btn--small"
disabled={busy}
onClick={() => void quickClaim(m.playerMissionId)}
>
Riscuoti
</button>
) : (
<Countdown target={m.completesAt} onDone={() => setTick((t) => t + 1)} />
)}
</li>
))}
</ul>
</section>
)}
<div className="quicklinks">
<Link to="/missions" className="btn btn--secondary">
🎯 Missioni
</Link>
<Link to="/market" className="btn btn--secondary">
Mercato
</Link>
<Link to="/leaderboard" className="btn btn--secondary">
🏆 Classifiche
</Link>
</div>
<section className="card">
<h3 className="card__title">Eventi in corso</h3>
{events.length === 0 ? (
@@ -78,9 +156,6 @@ export function DashboardScreen() {
)}
</section>
<Link to="/leaderboard" className="btn btn--secondary">
🏆 Classifiche
</Link>
<button type="button" className="btn btn--ghost" onClick={logout}>
Esci
</button>