This commit is contained in:
gpatruno
2026-07-18 17:40:39 +02:00
parent 2168998679
commit d0f868ffa5
19 changed files with 406 additions and 28 deletions
+192
View File
@@ -0,0 +1,192 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../api';
export default function AdminCategories() {
const [categories, setCategories] = useState([]);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const [query, setQuery] = useState('');
const [name, setName] = useState('');
const [editingId, setEditingId] = useState(null);
const [editName, setEditName] = useState('');
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const data = await api.adminCategories();
setCategories(data.categories || []);
}, []);
useEffect(() => {
load().catch((err) => setError(err.message));
}, [load]);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return categories;
return categories.filter((c) => c.name.toLowerCase().includes(q));
}, [categories, query]);
return (
<div>
<div className="section-head">
<div>
<h1>Categories</h1>
<p className="muted">
{categories.length} categories · rename updates items · delete only when empty
</p>
</div>
<input
className="admin-search"
placeholder="Search categories…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
{error && <div className="error">{error}</div>}
{message && <div className="ok-msg">{message}</div>}
<div className="admin-block">
<h3>Create category</h3>
<form
className="form admin-row"
onSubmit={async (e) => {
e.preventDefault();
setError('');
setMessage('');
setBusy(true);
try {
await api.createCategory({ name });
setName('');
setMessage('Category created.');
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}}
>
<label>
Name
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Rifle, Charm…"
required
/>
</label>
<button className="btn" type="submit" disabled={busy || !name.trim()}>
{busy ? 'Creating…' : 'Create'}
</button>
</form>
</div>
<div className="admin-block">
<h3>All categories</h3>
{filtered.length === 0 ? (
<p className="muted">No categories match.</p>
) : (
<ul className="admin-cat-list">
{filtered.map((cat) => (
<li key={cat.id} className="admin-cat-row">
{editingId === cat.id ? (
<form
className="admin-row admin-cat-edit"
onSubmit={async (e) => {
e.preventDefault();
setError('');
setMessage('');
setBusy(true);
try {
await api.updateCategory(cat.id, { name: editName });
setEditingId(null);
setEditName('');
setMessage('Category renamed.');
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}}
>
<input
value={editName}
onChange={(e) => setEditName(e.target.value)}
required
autoFocus
/>
<button className="btn" type="submit" disabled={busy}>
Save
</button>
<button
type="button"
className="btn ghost"
onClick={() => {
setEditingId(null);
setEditName('');
}}
>
Cancel
</button>
</form>
) : (
<>
<div className="admin-row-main">
<strong>{cat.name}</strong>
<span className="muted">
{cat.itemCount} item{cat.itemCount === 1 ? '' : 's'}
</span>
</div>
<div className="admin-row">
<button
type="button"
className="btn secondary"
onClick={() => {
setEditingId(cat.id);
setEditName(cat.name);
setError('');
setMessage('');
}}
>
Rename
</button>
<button
type="button"
className="btn ghost"
disabled={cat.itemCount > 0 || busy}
title={
cat.itemCount > 0
? `Used by ${cat.itemCount} item${cat.itemCount === 1 ? '' : 's'}`
: 'Delete category'
}
onClick={async () => {
if (cat.itemCount > 0) return;
setError('');
setMessage('');
setBusy(true);
try {
await api.deleteCategory(cat.id);
setMessage(`Deleted “${cat.name}”.`);
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}}
>
Delete
</button>
</div>
</>
)}
</li>
))}
</ul>
)}
</div>
</div>
);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default function AdminDashboard() {
<div className="section-head">
<div>
<h1>Overview</h1>
<p className="muted">Live snapshot of CaseForge.</p>
<p className="muted">Live snapshot of CaseOrion.</p>
</div>
</div>
+5 -1
View File
@@ -51,7 +51,11 @@ export default function AdminItems() {
]);
setItems(i.items);
setRarities(r.rarities);
setCategories(c.categories || []);
setCategories(
(c.names || c.categories || []).map((entry) =>
typeof entry === 'string' ? entry : entry.name
)
);
setItemForm((f) => ({
...f,
rarity: f.rarity || r.rarities[0] || 'MilSpec',
+2 -1
View File
@@ -6,6 +6,7 @@ const NAV = [
{ to: '/admin/players', label: 'Players' },
{ to: '/admin/cases', label: 'Cases' },
{ to: '/admin/items', label: 'Items' },
{ to: '/admin/categories', label: 'Categories' },
{ to: '/admin/drops', label: 'Drops' },
];
@@ -17,7 +18,7 @@ export default function AdminLayout() {
<div className="admin-shell">
<aside className="admin-sidebar">
<div className="admin-sidebar-brand">
<span className="brand">CaseForge</span>
<span className="brand">CaseOrion</span>
<span className="admin-sidebar-tag">Admin</span>
</div>
<nav className="admin-side-nav">