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 { Link } from 'react-router-dom';
|
||||||
import { api } from '../api/api';
|
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 { useAuth } from '../auth/AuthContext';
|
||||||
|
import { Countdown } from '../components/Countdown';
|
||||||
|
import { useToast } from '../components/Toast';
|
||||||
import { EVENT_TYPE_LABELS, formatMoney, riskLabel } from '../shared/format';
|
import { EVENT_TYPE_LABELS, formatMoney, riskLabel } from '../shared/format';
|
||||||
|
|
||||||
export function DashboardScreen() {
|
export function DashboardScreen() {
|
||||||
const { player, setPlayer, logout } = useAuth();
|
const { player, setPlayer, logout } = useAuth();
|
||||||
|
const { toast } = useToast();
|
||||||
const [me, setMe] = useState<PlayerMeResponse | null>(null);
|
const [me, setMe] = useState<PlayerMeResponse | null>(null);
|
||||||
const [events, setEvents] = useState<WorldEvent[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
void api.playerMe().then((res) => {
|
void reload();
|
||||||
setMe(res);
|
}, [reload]);
|
||||||
|
|
||||||
|
async function quickClaim(playerMissionId: string) {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const res = await api.claimMission(playerMissionId);
|
||||||
setPlayer(res.player);
|
setPlayer(res.player);
|
||||||
});
|
toast(
|
||||||
void api.events().then((res) => setEvents(res.events));
|
res.success
|
||||||
}, [setPlayer]);
|
? `✅ "${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;
|
if (!player) return null;
|
||||||
|
|
||||||
const xpNext = me?.xpForNextLevel ?? null;
|
const xpNext = me?.xpForNextLevel ?? null;
|
||||||
const xpRatio = xpNext ? Math.min(1, player.experience / xpNext) : 0;
|
const xpRatio = xpNext ? Math.min(1, player.experience / xpNext) : 0;
|
||||||
|
const isReady = (m: ActiveMission) => new Date(m.completesAt).getTime() <= Date.now();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="screen">
|
<div className="screen">
|
||||||
@@ -55,6 +93,46 @@ export function DashboardScreen() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</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">
|
<section className="card">
|
||||||
<h3 className="card__title">Eventi in corso</h3>
|
<h3 className="card__title">Eventi in corso</h3>
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
@@ -78,9 +156,6 @@ export function DashboardScreen() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Link to="/leaderboard" className="btn btn--secondary">
|
|
||||||
🏆 Classifiche
|
|
||||||
</Link>
|
|
||||||
<button type="button" className="btn btn--ghost" onClick={logout}>
|
<button type="button" className="btn btn--ghost" onClick={logout}>
|
||||||
Esci
|
Esci
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user