first commit
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { api, formatCredits } from '../api';
|
||||
import { useAuth } from '../AuthContext';
|
||||
|
||||
export default function Admin() {
|
||||
const { user, loading, adminLogin, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState('cases');
|
||||
const [cases, setCases] = useState([]);
|
||||
const [items, setItems] = useState([]);
|
||||
const [rarities, setRarities] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [caseForm, setCaseForm] = useState({ name: '', price: 100, imageUrl: '', active: true });
|
||||
const [itemForm, setItemForm] = useState({
|
||||
name: '',
|
||||
rarity: 'MilSpec',
|
||||
marketValue: 100,
|
||||
imageUrl: '',
|
||||
});
|
||||
const [dropForm, setDropForm] = useState({ caseId: '', itemId: '', dropRate: 1 });
|
||||
|
||||
const load = async () => {
|
||||
const [c, i, r] = await Promise.all([
|
||||
api.adminCases(),
|
||||
api.adminItems(),
|
||||
api.adminRarities(),
|
||||
]);
|
||||
setCases(c.cases);
|
||||
setItems(i.items);
|
||||
setRarities(r.rarities);
|
||||
if (!dropForm.caseId && c.cases[0]) {
|
||||
setDropForm((f) => ({ ...f, caseId: String(c.cases[0].id) }));
|
||||
}
|
||||
if (!dropForm.itemId && i.items[0]) {
|
||||
setDropForm((f) => ({ ...f, itemId: String(i.items[0].id) }));
|
||||
}
|
||||
if (r.rarities.length && !itemForm.rarity) {
|
||||
setItemForm((f) => ({ ...f, rarity: r.rarities[0] }));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== 'admin') return;
|
||||
load().catch((err) => setError(err.message));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user]);
|
||||
|
||||
if (loading) return <p className="muted">Loading…</p>;
|
||||
|
||||
if (!user || user.role !== 'admin') {
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setBusy(true);
|
||||
try {
|
||||
await adminLogin(username, password);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-panel">
|
||||
<div className="brand">CaseForge</div>
|
||||
<h1>Admin</h1>
|
||||
<p className="muted">Manage cases, items, and drop rates.</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input value={username} onChange={(e) => setUsername(e.target.value)} required />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && <div className="error">{error}</div>}
|
||||
<button className="btn" type="submit" disabled={busy}>
|
||||
{busy ? 'Signing in…' : 'Admin login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main" style={{ paddingTop: '1.5rem' }}>
|
||||
<header className="topnav" style={{ margin: '-1.5rem -1.5rem 1.5rem', position: 'static' }}>
|
||||
<span className="brand">CaseForge Admin</span>
|
||||
<nav className="nav-links">
|
||||
<Link to="/leaderboard">Leaderboard</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h1>Admin panel</h1>
|
||||
<p className="muted">Cases, loot pool, and drop weights.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-tabs">
|
||||
{['cases', 'items', 'drops'].map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
className={tab === t ? 'active' : ''}
|
||||
onClick={() => setTab(t)}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
{tab === 'cases' && (
|
||||
<div>
|
||||
<div className="admin-block">
|
||||
<h3>Create case</h3>
|
||||
<form
|
||||
className="admin-row"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.createCase({
|
||||
...caseForm,
|
||||
price: Number(caseForm.price),
|
||||
});
|
||||
setCaseForm({ name: '', price: 100, imageUrl: '', active: true });
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
placeholder="Name"
|
||||
value={caseForm.name}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Price (cents)"
|
||||
value={caseForm.price}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, price: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
placeholder="Image URL"
|
||||
value={caseForm.imageUrl}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, imageUrl: e.target.value })}
|
||||
/>
|
||||
<button className="btn" type="submit">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{cases.map((c) => (
|
||||
<div key={c.id} className="admin-block">
|
||||
<div className="admin-row">
|
||||
<strong>{c.name}</strong>
|
||||
<span className="muted">{formatCredits(c.price)} cr</span>
|
||||
<span className="muted">{c.active ? 'active' : 'inactive'}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary"
|
||||
onClick={async () => {
|
||||
await api.updateCase(c.id, { active: !c.active });
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
{c.active ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Delete case ${c.name}?`)) return;
|
||||
await api.deleteCase(c.id);
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div className="muted">{c.items.length} drops configured</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'items' && (
|
||||
<div>
|
||||
<div className="admin-block">
|
||||
<h3>Create item</h3>
|
||||
<form
|
||||
className="admin-row"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.createItem({
|
||||
...itemForm,
|
||||
marketValue: Number(itemForm.marketValue),
|
||||
});
|
||||
setItemForm({
|
||||
name: '',
|
||||
rarity: rarities[0] || 'MilSpec',
|
||||
marketValue: 100,
|
||||
imageUrl: '',
|
||||
});
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
placeholder="Name"
|
||||
value={itemForm.name}
|
||||
onChange={(e) => setItemForm({ ...itemForm, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<select
|
||||
value={itemForm.rarity}
|
||||
onChange={(e) => setItemForm({ ...itemForm, rarity: e.target.value })}
|
||||
>
|
||||
{rarities.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Value (cents)"
|
||||
value={itemForm.marketValue}
|
||||
onChange={(e) => setItemForm({ ...itemForm, marketValue: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<button className="btn" type="submit">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="admin-block">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="admin-row">
|
||||
<span>
|
||||
{item.name} · {item.rarity} · {formatCredits(item.marketValue)} cr
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Delete ${item.name}?`)) return;
|
||||
await api.deleteItem(item.id);
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'drops' && (
|
||||
<div>
|
||||
<div className="admin-block">
|
||||
<h3>Add drop to case</h3>
|
||||
<form
|
||||
className="admin-row"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.addCaseItem(Number(dropForm.caseId), {
|
||||
itemId: Number(dropForm.itemId),
|
||||
dropRate: Number(dropForm.dropRate),
|
||||
});
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={dropForm.caseId}
|
||||
onChange={(e) => setDropForm({ ...dropForm, caseId: e.target.value })}
|
||||
>
|
||||
{cases.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={dropForm.itemId}
|
||||
onChange={(e) => setDropForm({ ...dropForm, itemId: e.target.value })}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</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>
|
||||
|
||||
{cases.map((c) => (
|
||||
<div key={c.id} className="admin-block">
|
||||
<h3>{c.name}</h3>
|
||||
<div className="drop-list">
|
||||
{c.items.length === 0 && <div className="muted">No drops yet.</div>}
|
||||
{c.items.map((ci) => (
|
||||
<div key={ci.id} className="drop-row">
|
||||
<span>
|
||||
{ci.item.name} ({ci.item.rarity})
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.01"
|
||||
defaultValue={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="muted">weight</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
onClick={async () => {
|
||||
await api.deleteCaseItem(ci.id);
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user