prstige
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user