228 lines
7.3 KiB
React
228 lines
7.3 KiB
React
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { api, formatCredits, centsJson, toCentsBigInt } 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: centsJson(toCentsBigInt(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: centsJson(toCentsBigInt(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>
|
|
);
|
|
}
|