173 lines
5.8 KiB
React
173 lines
5.8 KiB
React
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { api, formatCredits } from '../api';
|
|
|
|
function dropPercents(items) {
|
|
const total = items.reduce((s, ci) => s + Number(ci.dropRate || 0), 0);
|
|
if (!(total > 0)) return items.map(() => 0);
|
|
return items.map((ci) => (Number(ci.dropRate) / total) * 100);
|
|
}
|
|
|
|
export default function AdminDrops() {
|
|
const [cases, setCases] = useState([]);
|
|
const [items, setItems] = useState([]);
|
|
const [error, setError] = useState('');
|
|
const [selectedCaseId, setSelectedCaseId] = useState('');
|
|
const [dropForm, setDropForm] = useState({ itemId: '', dropRate: 1 });
|
|
|
|
const load = useCallback(async () => {
|
|
const [c, i] = await Promise.all([api.adminCases(), api.adminItems()]);
|
|
setCases(c.cases);
|
|
setItems(i.items);
|
|
setSelectedCaseId((prev) => {
|
|
if (prev && c.cases.some((x) => String(x.id) === String(prev))) return prev;
|
|
return c.cases[0] ? String(c.cases[0].id) : '';
|
|
});
|
|
setDropForm((f) => ({
|
|
...f,
|
|
itemId: f.itemId || (i.items[0] ? String(i.items[0].id) : ''),
|
|
}));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load().catch((err) => setError(err.message));
|
|
}, [load]);
|
|
|
|
const selected = useMemo(
|
|
() => cases.find((c) => String(c.id) === String(selectedCaseId)) || null,
|
|
[cases, selectedCaseId]
|
|
);
|
|
|
|
const poolItems = useMemo(() => {
|
|
if (!selected) return [];
|
|
return [...selected.items].sort((a, b) => Number(a.dropRate) - Number(b.dropRate));
|
|
}, [selected]);
|
|
|
|
const percents = useMemo(() => dropPercents(poolItems), [poolItems]);
|
|
|
|
return (
|
|
<div>
|
|
<div className="section-head">
|
|
<div>
|
|
<h1>Drop rates</h1>
|
|
<p className="muted">Weights per case · % shown from relative weights</p>
|
|
</div>
|
|
<select
|
|
className="admin-search"
|
|
value={selectedCaseId}
|
|
onChange={(e) => setSelectedCaseId(e.target.value)}
|
|
aria-label="Select case"
|
|
>
|
|
{cases.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name} ({c.items.length})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{error && <div className="error">{error}</div>}
|
|
|
|
{!selected ? (
|
|
<div className="empty">Create a case first.</div>
|
|
) : (
|
|
<>
|
|
<div className="admin-block">
|
|
<h3>
|
|
Add drop · {selected.name}{' '}
|
|
<span className="muted">({formatCredits(selected.price)} cr)</span>
|
|
</h3>
|
|
<form
|
|
className="admin-row"
|
|
onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
try {
|
|
await api.addCaseItem(Number(selectedCaseId), {
|
|
itemId: Number(dropForm.itemId),
|
|
dropRate: Number(dropForm.dropRate),
|
|
});
|
|
await load();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
}}
|
|
>
|
|
<select
|
|
value={dropForm.itemId}
|
|
onChange={(e) => setDropForm({ ...dropForm, itemId: e.target.value })}
|
|
>
|
|
{items.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.name} ({item.rarity})
|
|
</option>
|
|
))}
|
|
</select>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
min="0.01"
|
|
value={dropForm.dropRate}
|
|
onChange={(e) => setDropForm({ ...dropForm, dropRate: e.target.value })}
|
|
placeholder="Weight"
|
|
required
|
|
/>
|
|
<button className="btn" type="submit">
|
|
Add drop
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="admin-block">
|
|
<h3>Pool ({poolItems.length})</h3>
|
|
{poolItems.length === 0 ? (
|
|
<div className="muted">No drops yet.</div>
|
|
) : (
|
|
<div className="drop-list">
|
|
{poolItems.map((ci, idx) => (
|
|
<div key={ci.id} className="drop-row">
|
|
<span className="drop-item-cell">
|
|
{ci.item.imageUrl ? (
|
|
<img src={ci.item.imageUrl} alt="" className="drop-thumb" />
|
|
) : null}
|
|
{ci.item.name}{' '}
|
|
<span className="muted">({ci.item.rarity})</span>
|
|
</span>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
min="0.01"
|
|
defaultValue={ci.dropRate}
|
|
key={`${ci.id}-${ci.dropRate}`}
|
|
onBlur={async (e) => {
|
|
const dropRate = Number(e.target.value);
|
|
if (!(dropRate > 0) || dropRate === ci.dropRate) return;
|
|
try {
|
|
await api.updateCaseItem(ci.id, { dropRate });
|
|
await load();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
}}
|
|
/>
|
|
<span className="admin-drop-pct">{percents[idx].toFixed(2)}%</span>
|
|
<button
|
|
type="button"
|
|
className="btn danger"
|
|
onClick={async () => {
|
|
await api.deleteCaseItem(ci.id);
|
|
await load();
|
|
}}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|