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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user