Files
gioco-mobile/client/src/screens/MarketScreen.tsx
T
davideandClaude Sonnet 4.6 6abd115c81 client: market trend tags + leaderboard live updates via Socket.IO
Market rows show a TrendTag: 💰 affare when buyPrice < basePrice,
📈 vendi when sellPrice > basePrice. Leaderboard subscribes to
leaderboard:updated socket event to refresh without polling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 00:12:14 +02:00

175 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import { api } from '../api/api';
import { ApiError } from '../api/http';
import type { InventoryResponse, MarketEntry, MarketResponse } from '../api/types';
import { useAuth } from '../auth/AuthContext';
import { useToast } from '../components/Toast';
import { getSocket } from '../realtime/socket';
import { formatMoney } from '../shared/format';
/**
* Confronta i prezzi locali col valore base della merce: segnala dove
* conviene comprare (buy sotto il base) e dove conviene vendere (sell sopra).
*/
function TrendTag({ entry }: { entry: MarketEntry }) {
const buyDelta = Math.round((entry.buyPrice / entry.basePrice - 1) * 100);
const sellDelta = Math.round((entry.sellPrice / entry.basePrice - 1) * 100);
if (sellDelta > 0) {
return <span className="trend trend--sell">📈 vendi +{sellDelta}%</span>;
}
if (buyDelta < 0) {
return <span className="trend trend--buy">💰 affare {buyDelta}%</span>;
}
return null;
}
export function MarketScreen() {
const { player, refreshPlayer } = useAuth();
const { toast } = useToast();
const [market, setMarket] = useState<MarketResponse | null>(null);
const [inventory, setInventory] = useState<InventoryResponse | null>(null);
const [selected, setSelected] = useState<MarketEntry | null>(null);
const [quantity, setQuantity] = useState(1);
const [busy, setBusy] = useState(false);
const reload = useCallback(async () => {
const [m, inv] = await Promise.all([api.market(), api.inventory()]);
setMarket(m);
setInventory(inv);
}, []);
useEffect(() => {
void reload();
}, [reload]);
// Il mercato si aggiorna a ogni tick del server
useEffect(() => {
const socket = getSocket();
if (!socket) return;
const onUpdate = () => void reload();
socket.on('market:updated', onUpdate);
return () => {
socket.off('market:updated', onUpdate);
};
}, [reload]);
const owned = (itemId: string) =>
inventory?.items.find((i) => i.itemId === itemId)?.quantity ?? 0;
async function trade(kind: 'buy' | 'sell') {
if (!selected) return;
setBusy(true);
try {
const fn = kind === 'buy' ? api.buy : api.sell;
const res = await fn(selected.itemId, quantity);
toast(
kind === 'buy'
? `Comprati ${res.quantity}× ${res.itemName} per ${formatMoney(res.total)}`
: `Venduti ${res.quantity}× ${res.itemName} per ${formatMoney(res.total)}`,
'success',
);
setSelected(null);
setQuantity(1);
await Promise.all([reload(), refreshPlayer()]);
} catch (err) {
toast(err instanceof ApiError ? err.message : 'Errore imprevisto', 'error');
} finally {
setBusy(false);
}
}
if (!market) return <p className="muted screen">Caricamento mercato...</p>;
return (
<div className="screen">
<h2 className="screen__title">Mercato di {market.city.name}</h2>
{market.activeEvents.length > 0 && (
<div className="banner">
{market.activeEvents.map((e) => (
<span key={e.id}> {e.title}</span>
))}
</div>
)}
<ul className="list">
{market.prices.map((entry) => (
<li
key={entry.itemId}
className="list__row list__row--clickable"
onClick={() => {
setSelected(entry);
setQuantity(1);
}}
>
<div>
<strong>{entry.name}</strong>
{owned(entry.itemId) > 0 && (
<span className="badge badge--small">×{owned(entry.itemId)}</span>
)}
</div>
<div className="prices">
<span className="price price--buy">{formatMoney(entry.buyPrice)}</span>
<span className="price price--sell">{formatMoney(entry.sellPrice)}</span>
<TrendTag entry={entry} />
</div>
</li>
))}
</ul>
<p className="muted small">
Prezzo a sinistra: acquisto · a destra: vendita. I prezzi cambiano ogni minuto: compra
dove c'è l'affare, vendi dove rende.
</p>
{selected && (
<div className="modal-backdrop" onClick={() => setSelected(null)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h3>{selected.name}</h3>
<p className="muted small">
Possiedi: {owned(selected.itemId)} · peso {selected.weight}/unità
</p>
<label className="quantity">
Quantità
<div className="quantity__controls">
<button type="button" onClick={() => setQuantity((q) => Math.max(1, q - 1))}>
</button>
<input
type="number"
min={1}
max={100}
value={quantity}
onChange={(e) =>
setQuantity(Math.max(1, Math.min(100, Number(e.target.value) || 1)))
}
/>
<button type="button" onClick={() => setQuantity((q) => Math.min(100, q + 1))}>
+
</button>
</div>
</label>
<div className="modal__actions">
<button
type="button"
className="btn btn--primary"
disabled={busy || (player?.money ?? 0) < selected.buyPrice * quantity}
onClick={() => void trade('buy')}
>
Compra · {formatMoney(selected.buyPrice * quantity)}
</button>
<button
type="button"
className="btn btn--secondary"
disabled={busy || owned(selected.itemId) < quantity}
onClick={() => void trade('sell')}
>
Vendi · {formatMoney(selected.sellPrice * quantity)}
</button>
</div>
</div>
</div>
)}
</div>
);
}