nice update
This commit is contained in:
+106
-33
@@ -1,25 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, Navigate } from 'react-router-dom';
|
||||
import { api, formatCredits } from '../api';
|
||||
import { useAuth } from '../AuthContext';
|
||||
import ItemTile from '../components/ItemTile';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user, loading } = useAuth();
|
||||
const { user, loading, patchUser } = useAuth();
|
||||
const [inventory, setInventory] = useState([]);
|
||||
const [cases, setCases] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [sort, setSort] = useState('recent');
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
const inv = await api.inventory();
|
||||
setInventory(inv.inventory);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.role === 'admin') return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const [inv, cs] = await Promise.all([api.inventory(), api.cases()]);
|
||||
if (!cancelled) {
|
||||
setInventory(inv.inventory);
|
||||
setCases(cs.cases);
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(err.message);
|
||||
}
|
||||
@@ -29,6 +32,46 @@ export default function Dashboard() {
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const list = [...inventory];
|
||||
if (sort === 'value') {
|
||||
list.sort((a, b) => b.item.marketValue - a.item.marketValue);
|
||||
} else {
|
||||
list.sort((a, b) => new Date(b.obtainedAt) - new Date(a.obtainedAt));
|
||||
}
|
||||
return list;
|
||||
}, [inventory, sort]);
|
||||
|
||||
const selectedValue = useMemo(
|
||||
() =>
|
||||
inventory
|
||||
.filter((inv) => selected.includes(inv.id))
|
||||
.reduce((sum, inv) => sum + inv.item.marketValue, 0),
|
||||
[inventory, selected]
|
||||
);
|
||||
|
||||
const toggle = (id) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const sell = async () => {
|
||||
if (!selected.length) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await api.sellInventory(selected);
|
||||
patchUser({ balance: data.user.balance });
|
||||
setSelected([]);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p className="muted">Loading…</p>;
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
if (user.role === 'admin') return <Navigate to="/admin" replace />;
|
||||
@@ -39,42 +82,72 @@ export default function Dashboard() {
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h1>Inventory</h1>
|
||||
<p className="muted">Your drops and loot.</p>
|
||||
<p className="muted">Select items to sell at market value.</p>
|
||||
</div>
|
||||
<div className="balance-pill">{formatCredits(user.balance)} cr</div>
|
||||
</div>
|
||||
|
||||
<div className="inv-toolbar">
|
||||
<div className="sort-toggle" role="group" aria-label="Sort inventory">
|
||||
<button
|
||||
type="button"
|
||||
className={sort === 'recent' ? 'active' : ''}
|
||||
onClick={() => setSort('recent')}
|
||||
>
|
||||
Most recent
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={sort === 'value' ? 'active' : ''}
|
||||
onClick={() => setSort('value')}
|
||||
>
|
||||
Highest value
|
||||
</button>
|
||||
</div>
|
||||
<div className="row-actions">
|
||||
{selected.length > 0 && (
|
||||
<>
|
||||
<span className="muted">
|
||||
{selected.length} selected · {formatCredits(selectedValue)} cr
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
onClick={() => setSelected([])}
|
||||
disabled={busy}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={sell} disabled={busy}>
|
||||
Sell
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
{inventory.length === 0 ? (
|
||||
<div className="empty">Empty for now — open a case below.</div>
|
||||
<div className="empty">
|
||||
Empty for now —{' '}
|
||||
<Link to="/cases" style={{ color: 'var(--accent)' }}>
|
||||
open a case
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid">
|
||||
{inventory.map((inv) => (
|
||||
<ItemTile key={inv.id} item={inv.item} />
|
||||
{sorted.map((inv) => (
|
||||
<ItemTile
|
||||
key={inv.id}
|
||||
item={inv.item}
|
||||
selected={selected.includes(inv.id)}
|
||||
onClick={() => toggle(inv.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h2>Cases</h2>
|
||||
<p className="muted">Pick a crate to open.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid case-grid">
|
||||
{cases.map((c) => (
|
||||
<Link key={c.id} to={`/cases/${c.id}`} className="case-tile">
|
||||
<div className="case-visual" style={{ color: 'var(--accent)' }}>
|
||||
▣
|
||||
</div>
|
||||
<div className="case-name">{c.name}</div>
|
||||
<div className="case-meta">{formatCredits(c.price)} cr</div>
|
||||
<div className="case-meta">{c.items.length} possible drops</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user