admin
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>CaseForge</title>
|
||||
<title>CaseOrion</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -139,6 +139,12 @@ export const api = {
|
||||
adminItems: () => request('/api/admin/items'),
|
||||
adminRarities: () => request('/api/admin/rarities'),
|
||||
adminCategories: () => request('/api/admin/categories'),
|
||||
createCategory: (body) =>
|
||||
request('/api/admin/categories', { method: 'POST', body: JSON.stringify(body) }),
|
||||
updateCategory: (id, body) =>
|
||||
request(`/api/admin/categories/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
deleteCategory: (id) =>
|
||||
request(`/api/admin/categories/${id}`, { method: 'DELETE' }),
|
||||
createCase: (body) =>
|
||||
request('/api/admin/cases', { method: 'POST', body: JSON.stringify(body) }),
|
||||
updateCase: (id, body) =>
|
||||
|
||||
@@ -7,7 +7,7 @@ import SessionLimitOverlay from './SessionLimitOverlay';
|
||||
import { onInventoryChanged } from '../inventoryEvents';
|
||||
import { getSocket, onSessionLimitChange } from '../socket';
|
||||
|
||||
const FEED_KEY = 'caseforge-feed-drop';
|
||||
const FEED_KEY = 'caseorion-feed-drop';
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
@@ -168,7 +168,7 @@ export default function Layout() {
|
||||
<header className="topnav">
|
||||
<div className="topnav-left">
|
||||
<NavLink to="/dashboard" className="brand">
|
||||
CaseForge
|
||||
CaseOrion
|
||||
</NavLink>
|
||||
{isPlayer && (
|
||||
<button
|
||||
|
||||
@@ -1649,6 +1649,42 @@ a.balance-pill-link.active {
|
||||
padding: 0.1rem 0.4rem;
|
||||
}
|
||||
|
||||
.admin-cat-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.admin-cat-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.65rem 1rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border: 1px solid var(--stroke);
|
||||
background: var(--bg-deep);
|
||||
}
|
||||
|
||||
.admin-cat-row .admin-row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.admin-cat-edit {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-cat-edit input {
|
||||
flex: 1;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.admin-drop-pct {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.85rem;
|
||||
|
||||
@@ -5,6 +5,7 @@ import AdminLayout from '../admin/AdminLayout';
|
||||
import AdminDashboard from '../admin/AdminDashboard';
|
||||
import AdminCases from '../admin/AdminCases';
|
||||
import AdminItems from '../admin/AdminItems';
|
||||
import AdminCategories from '../admin/AdminCategories';
|
||||
import AdminDrops from '../admin/AdminDrops';
|
||||
import AdminPlayers from '../admin/AdminPlayers';
|
||||
|
||||
@@ -31,9 +32,9 @@ function AdminLogin() {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-panel">
|
||||
<div className="brand">CaseForge</div>
|
||||
<div className="brand">CaseOrion</div>
|
||||
<h1>Admin</h1>
|
||||
<p className="muted">Cases, items, drops, players & overview.</p>
|
||||
<p className="muted">Cases, items, categories, drops, players & overview.</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<label>
|
||||
Username
|
||||
@@ -71,6 +72,7 @@ export default function Admin() {
|
||||
<Route path="players" element={<AdminPlayers />} />
|
||||
<Route path="cases" element={<AdminCases />} />
|
||||
<Route path="items" element={<AdminItems />} />
|
||||
<Route path="categories" element={<AdminCategories />} />
|
||||
<Route path="drops" element={<AdminDrops />} />
|
||||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||||
</Route>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Login() {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-panel">
|
||||
<div className="brand">CaseForge</div>
|
||||
<div className="brand">CaseOrion</div>
|
||||
<h1>Login</h1>
|
||||
<p className="muted">Open crates. Build your inventory.</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
|
||||
@@ -257,7 +257,7 @@ export default function ProfilePage() {
|
||||
<section className="section admin-block">
|
||||
<h2>Session</h2>
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>
|
||||
Sign out of CaseForge on this device.
|
||||
Sign out of CaseOrion on this device.
|
||||
</p>
|
||||
<button type="button" className="btn danger" onClick={onLogout} disabled={busy}>
|
||||
Logout
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function Register() {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-panel">
|
||||
<div className="brand">CaseForge</div>
|
||||
<div className="brand">CaseOrion</div>
|
||||
<h1>Register</h1>
|
||||
<p className="muted">Create an account and get starting credits.</p>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const PREFIX = 'caseforge-pref';
|
||||
const PREFIX = 'caseorion-pref';
|
||||
|
||||
function storageKey(userId, suffix) {
|
||||
return `${PREFIX}-${userId}-${suffix}`;
|
||||
|
||||
Reference in New Issue
Block a user