This commit is contained in:
gpatruno
2026-07-18 17:25:56 +02:00
parent 2614d679c8
commit 2168998679
26 changed files with 2760 additions and 894 deletions
+170
View File
@@ -0,0 +1,170 @@
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 percents = useMemo(
() => (selected ? dropPercents(selected.items) : []),
[selected]
);
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 ({selected.items.length})</h3>
{selected.items.length === 0 ? (
<div className="muted">No drops yet.</div>
) : (
<div className="drop-list">
{selected.items.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>
);
}