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
+227
View File
@@ -0,0 +1,227 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { api, formatCredits } from '../api';
import ImageField from './ImageField';
const emptyCase = { name: '', price: 100, imageUrl: '', active: true };
export default function AdminCases() {
const [cases, setCases] = useState([]);
const [error, setError] = useState('');
const [query, setQuery] = useState('');
const [caseForm, setCaseForm] = useState(emptyCase);
const [editingId, setEditingId] = useState(null);
const [edit, setEdit] = useState(null);
const load = useCallback(async () => {
const data = await api.adminCases();
setCases(data.cases);
}, []);
useEffect(() => {
load().catch((err) => setError(err.message));
}, [load]);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return cases;
return cases.filter((c) => c.name.toLowerCase().includes(q));
}, [cases, query]);
const startEdit = (c) => {
setEditingId(c.id);
setEdit({
name: c.name,
price: c.price,
imageUrl: c.imageUrl || '',
active: c.active,
});
};
return (
<div>
<div className="section-head">
<div>
<h1>Cases</h1>
<p className="muted">{cases.length} crates · create, enable, price & art</p>
</div>
<input
className="admin-search"
placeholder="Search cases…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
{error && <div className="error">{error}</div>}
<div className="admin-block">
<h3>Create case</h3>
<form
className="form admin-form-grid"
onSubmit={async (e) => {
e.preventDefault();
setError('');
try {
await api.createCase({ ...caseForm, price: Number(caseForm.price) });
setCaseForm(emptyCase);
await load();
} catch (err) {
setError(err.message);
}
}}
>
<label>
Name
<input
value={caseForm.name}
onChange={(e) => setCaseForm({ ...caseForm, name: e.target.value })}
required
/>
</label>
<label>
Price (cents)
<input
type="number"
value={caseForm.price}
onChange={(e) => setCaseForm({ ...caseForm, price: e.target.value })}
required
/>
</label>
<label className="admin-form-span">
Image
<ImageField
value={caseForm.imageUrl}
onChange={(imageUrl) => setCaseForm({ ...caseForm, imageUrl })}
/>
</label>
<button className="btn" type="submit">
Add case
</button>
</form>
</div>
<div className="admin-card-list">
{filtered.map((c) => (
<div key={c.id} className="admin-block">
{editingId === c.id && edit ? (
<form
className="form admin-form-grid"
onSubmit={async (e) => {
e.preventDefault();
setError('');
try {
await api.updateCase(editingId, {
name: edit.name,
price: Number(edit.price),
imageUrl: edit.imageUrl,
active: edit.active,
});
setEditingId(null);
setEdit(null);
await load();
} catch (err) {
setError(err.message);
}
}}
>
<h3 className="admin-form-span">Edit #{c.id}</h3>
<label>
Name
<input
value={edit.name}
onChange={(e) => setEdit({ ...edit, name: e.target.value })}
required
/>
</label>
<label>
Price (cents)
<input
type="number"
value={edit.price}
onChange={(e) => setEdit({ ...edit, price: e.target.value })}
required
/>
</label>
<label className="admin-form-span">
Image
<ImageField
value={edit.imageUrl}
onChange={(imageUrl) => setEdit({ ...edit, imageUrl })}
/>
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={edit.active}
onChange={(e) => setEdit({ ...edit, active: e.target.checked })}
/>
Active
</label>
<div className="row-actions admin-form-span">
<button className="btn" type="submit">
Save
</button>
<button
type="button"
className="btn secondary"
onClick={() => {
setEditingId(null);
setEdit(null);
}}
>
Cancel
</button>
</div>
</form>
) : (
<>
<div className="admin-row">
{c.imageUrl ? (
<div className="admin-thumb">
<img src={c.imageUrl} alt="" />
</div>
) : (
<div className="admin-thumb empty"></div>
)}
<div className="admin-row-main">
<strong>{c.name}</strong>
<div className="muted">
{formatCredits(c.price)} cr · {c.items.length} drops ·{' '}
<span className={c.active ? 'admin-pill-on' : 'admin-pill-off'}>
{c.active ? 'active' : 'inactive'}
</span>
</div>
</div>
<button type="button" className="btn secondary" onClick={() => startEdit(c)}>
Edit
</button>
<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>
))}
</div>
</div>
);
}