141 lines
4.4 KiB
React
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>
|
|
);
|
|
}
|