Files
CaseGambling/client/src/pages/CasePage.jsx
T
2026-07-16 20:23:58 +02:00

141 lines
4.4 KiB
React

import { useEffect, useState } from 'react';
import { Link, Navigate, useParams } from 'react-router-dom';
import { api, formatCredits } from '../api';
import { useAuth } from '../AuthContext';
import CaseReel from '../components/CaseReel';
import ItemTile from '../components/ItemTile';
import { rarityColor } from '../rarities';
export default function CasePage() {
const { id } = useParams();
const { user, loading, updateBalance } = useAuth();
const [crate, setCrate] = useState(null);
const [error, setError] = useState('');
const [spinning, setSpinning] = useState(false);
const [winnerId, setWinnerId] = useState(null);
const [wonItem, setWonItem] = useState(null);
const [showResult, setShowResult] = useState(false);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const data = await api.case(id);
if (!cancelled) setCrate(data.case);
} catch (err) {
if (!cancelled) setError(err.message);
}
})();
return () => {
cancelled = true;
};
}, [id]);
if (loading) return <p className="muted">Loading</p>;
if (!user) return <Navigate to="/login" replace />;
if (user.role === 'admin') return <Navigate to="/admin" replace />;
const canOpen =
crate && !spinning && user.balance >= crate.price && crate.items.length > 0;
const openCase = async () => {
if (!canOpen) return;
setError('');
setWonItem(null);
setShowResult(false);
setWinnerId(null);
setSpinning(false);
try {
const result = await api.openCase(crate.id);
updateBalance(result.user.balance);
setWonItem(result.item);
setWinnerId(result.item.id);
// start spin on next frame so reel rebuilds with winner
requestAnimationFrame(() => setSpinning(true));
} catch (err) {
setError(err.message);
}
};
return (
<div className="case-page">
<Link to="/cases" className="muted">
Back to cases
</Link>
{!crate && !error && <p className="muted">Loading case</p>}
{error && <div className="error">{error}</div>}
{crate && (
<>
<div className="case-hero">
<h1>{crate.name}</h1>
<p className="muted">
Cost {formatCredits(crate.price)} cr · Balance {formatCredits(user.balance)} cr
</p>
<button
type="button"
className={`case-icon ${canOpen ? '' : 'disabled'}${crate.imageUrl ? ' has-img' : ''}`}
onClick={openCase}
disabled={!canOpen}
title="Open case"
>
{crate.imageUrl ? <img src={crate.imageUrl} alt="" /> : '▣'}
</button>
<button className="btn" type="button" onClick={openCase} disabled={!canOpen}>
{spinning ? 'Opening…' : `Open for ${formatCredits(crate.price)} cr`}
</button>
</div>
{(spinning || winnerId) && (
<CaseReel
items={crate.items}
winnerId={winnerId}
spinning={spinning}
onDone={() => {
setSpinning(false);
setShowResult(true);
}}
/>
)}
{showResult && wonItem && (
<div
className="won-banner"
style={{ '--rarity': rarityColor(wonItem.rarity) }}
>
{wonItem.imageUrl ? (
<img src={wonItem.imageUrl} alt="" className="won-img" />
) : null}
<div className="muted">You unboxed</div>
<strong>{wonItem.name}</strong>
<div style={{ color: rarityColor(wonItem.rarity) }}>{wonItem.rarity}</div>
<div className="muted">{formatCredits(wonItem.marketValue)} cr</div>
</div>
)}
<section className="section">
<div className="section-head">
<div>
<h2>Possible drops</h2>
<p className="muted">Relative drop odds for this case.</p>
</div>
</div>
<div className="grid">
{crate.items.map((item) => (
<div key={item.id}>
<ItemTile item={item} />
<div className="item-meta" style={{ marginTop: '0.35rem' }}>
{item.chance}%
</div>
</div>
))}
</div>
</section>
</>
)}
</div>
);
}