157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
|
|
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';
|
|||
|
|
|
|||
|
|
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>
|
|||
|
|
</div>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
<p className="muted small">
|
|||
|
|
Prezzo a sinistra: acquisto · a destra: vendita. I prezzi cambiano ogni minuto.
|
|||
|
|
</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>
|
|||
|
|
);
|
|||
|
|
}
|