nice update
This commit is contained in:
+365
-107
@@ -3,6 +3,55 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { api, formatCredits } from '../api';
|
||||
import { useAuth } from '../AuthContext';
|
||||
|
||||
const emptyCase = { name: '', price: 100, imageUrl: '', active: true };
|
||||
const emptyItem = { name: '', rarity: 'MilSpec', marketValue: 100, imageUrl: '' };
|
||||
|
||||
function ImageField({ value, onChange, disabled }) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const onFile = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
const data = await api.adminUpload(file);
|
||||
onChange(data.url);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-field">
|
||||
{value ? (
|
||||
<div className="admin-thumb">
|
||||
<img src={value} alt="" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-thumb empty">No img</div>
|
||||
)}
|
||||
<input
|
||||
placeholder="Image URL or upload"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<label className="btn secondary file-btn">
|
||||
{uploading ? '…' : 'Upload'}
|
||||
<input type="file" accept="image/*" hidden onChange={onFile} disabled={disabled || uploading} />
|
||||
</label>
|
||||
{value ? (
|
||||
<button type="button" className="btn ghost" onClick={() => onChange('')} disabled={disabled}>
|
||||
Clear
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Admin() {
|
||||
const { user, loading, adminLogin, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@@ -15,13 +64,12 @@ export default function Admin() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [caseForm, setCaseForm] = useState({ name: '', price: 100, imageUrl: '', active: true });
|
||||
const [itemForm, setItemForm] = useState({
|
||||
name: '',
|
||||
rarity: 'MilSpec',
|
||||
marketValue: 100,
|
||||
imageUrl: '',
|
||||
});
|
||||
const [caseForm, setCaseForm] = useState(emptyCase);
|
||||
const [itemForm, setItemForm] = useState(emptyItem);
|
||||
const [editingCaseId, setEditingCaseId] = useState(null);
|
||||
const [editingItemId, setEditingItemId] = useState(null);
|
||||
const [caseEdit, setCaseEdit] = useState(null);
|
||||
const [itemEdit, setItemEdit] = useState(null);
|
||||
const [dropForm, setDropForm] = useState({ caseId: '', itemId: '', dropRate: 1 });
|
||||
|
||||
const load = async () => {
|
||||
@@ -39,8 +87,8 @@ export default function Admin() {
|
||||
if (!dropForm.itemId && i.items[0]) {
|
||||
setDropForm((f) => ({ ...f, itemId: String(i.items[0].id) }));
|
||||
}
|
||||
if (r.rarities.length && !itemForm.rarity) {
|
||||
setItemForm((f) => ({ ...f, rarity: r.rarities[0] }));
|
||||
if (r.rarities.length) {
|
||||
setItemForm((f) => ({ ...f, rarity: f.rarity || r.rarities[0] }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,6 +144,62 @@ export default function Admin() {
|
||||
);
|
||||
}
|
||||
|
||||
const startEditCase = (c) => {
|
||||
setEditingCaseId(c.id);
|
||||
setCaseEdit({
|
||||
name: c.name,
|
||||
price: c.price,
|
||||
imageUrl: c.imageUrl || '',
|
||||
active: c.active,
|
||||
});
|
||||
};
|
||||
|
||||
const saveCaseEdit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
try {
|
||||
await api.updateCase(editingCaseId, {
|
||||
name: caseEdit.name,
|
||||
price: Number(caseEdit.price),
|
||||
imageUrl: caseEdit.imageUrl,
|
||||
active: caseEdit.active,
|
||||
});
|
||||
setEditingCaseId(null);
|
||||
setCaseEdit(null);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditItem = (item) => {
|
||||
setEditingItemId(item.id);
|
||||
setItemEdit({
|
||||
name: item.name,
|
||||
rarity: item.rarity,
|
||||
marketValue: item.marketValue,
|
||||
imageUrl: item.imageUrl || '',
|
||||
});
|
||||
};
|
||||
|
||||
const saveItemEdit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
try {
|
||||
await api.updateItem(editingItemId, {
|
||||
name: itemEdit.name,
|
||||
rarity: itemEdit.rarity,
|
||||
marketValue: Number(itemEdit.marketValue),
|
||||
imageUrl: itemEdit.imageUrl,
|
||||
});
|
||||
setEditingItemId(null);
|
||||
setItemEdit(null);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="main" style={{ paddingTop: '1.5rem' }}>
|
||||
<header className="topnav" style={{ margin: '-1.5rem -1.5rem 1.5rem', position: 'static' }}>
|
||||
@@ -117,7 +221,7 @@ export default function Admin() {
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h1>Admin panel</h1>
|
||||
<p className="muted">Cases, loot pool, and drop weights.</p>
|
||||
<p className="muted">Cases, loot pool, images, and drop weights.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +245,7 @@ export default function Admin() {
|
||||
<div className="admin-block">
|
||||
<h3>Create case</h3>
|
||||
<form
|
||||
className="admin-row"
|
||||
className="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -149,66 +253,135 @@ export default function Admin() {
|
||||
...caseForm,
|
||||
price: Number(caseForm.price),
|
||||
});
|
||||
setCaseForm({ name: '', price: 100, imageUrl: '', active: true });
|
||||
setCaseForm(emptyCase);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
placeholder="Name"
|
||||
value={caseForm.name}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Price (cents)"
|
||||
value={caseForm.price}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, price: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
placeholder="Image URL"
|
||||
value={caseForm.imageUrl}
|
||||
onChange={(e) => setCaseForm({ ...caseForm, imageUrl: e.target.value })}
|
||||
/>
|
||||
<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>
|
||||
Image
|
||||
<ImageField
|
||||
value={caseForm.imageUrl}
|
||||
onChange={(imageUrl) => setCaseForm({ ...caseForm, imageUrl })}
|
||||
/>
|
||||
</label>
|
||||
<button className="btn" type="submit">
|
||||
Add
|
||||
Add case
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{cases.map((c) => (
|
||||
<div key={c.id} className="admin-block">
|
||||
<div className="admin-row">
|
||||
<strong>{c.name}</strong>
|
||||
<span className="muted">{formatCredits(c.price)} cr</span>
|
||||
<span className="muted">{c.active ? 'active' : 'inactive'}</span>
|
||||
<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 className="muted">{c.items.length} drops configured</div>
|
||||
{editingCaseId === c.id && caseEdit ? (
|
||||
<form className="form" onSubmit={saveCaseEdit}>
|
||||
<h3>Edit case #{c.id}</h3>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
value={caseEdit.name}
|
||||
onChange={(e) => setCaseEdit({ ...caseEdit, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Price (cents)
|
||||
<input
|
||||
type="number"
|
||||
value={caseEdit.price}
|
||||
onChange={(e) => setCaseEdit({ ...caseEdit, price: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Image
|
||||
<ImageField
|
||||
value={caseEdit.imageUrl}
|
||||
onChange={(imageUrl) => setCaseEdit({ ...caseEdit, imageUrl })}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={caseEdit.active}
|
||||
onChange={(e) => setCaseEdit({ ...caseEdit, active: e.target.checked })}
|
||||
/>
|
||||
Active
|
||||
</label>
|
||||
<div className="row-actions">
|
||||
<button className="btn" type="submit">
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary"
|
||||
onClick={() => {
|
||||
setEditingCaseId(null);
|
||||
setCaseEdit(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="admin-row">
|
||||
{c.imageUrl ? (
|
||||
<div className="admin-thumb">
|
||||
<img src={c.imageUrl} alt="" />
|
||||
</div>
|
||||
) : null}
|
||||
<strong>{c.name}</strong>
|
||||
<span className="muted">{formatCredits(c.price)} cr</span>
|
||||
<span className="muted">{c.active ? 'active' : 'inactive'}</span>
|
||||
<button type="button" className="btn secondary" onClick={() => startEditCase(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 className="muted">{c.items.length} drops configured</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -219,7 +392,7 @@ export default function Admin() {
|
||||
<div className="admin-block">
|
||||
<h3>Create item</h3>
|
||||
<form
|
||||
className="admin-row"
|
||||
className="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -228,10 +401,8 @@ export default function Admin() {
|
||||
marketValue: Number(itemForm.marketValue),
|
||||
});
|
||||
setItemForm({
|
||||
name: '',
|
||||
...emptyItem,
|
||||
rarity: rarities[0] || 'MilSpec',
|
||||
marketValue: 100,
|
||||
imageUrl: '',
|
||||
});
|
||||
await load();
|
||||
} catch (err) {
|
||||
@@ -239,55 +410,139 @@ export default function Admin() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
placeholder="Name"
|
||||
value={itemForm.name}
|
||||
onChange={(e) => setItemForm({ ...itemForm, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<select
|
||||
value={itemForm.rarity}
|
||||
onChange={(e) => setItemForm({ ...itemForm, rarity: e.target.value })}
|
||||
>
|
||||
{rarities.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Value (cents)"
|
||||
value={itemForm.marketValue}
|
||||
onChange={(e) => setItemForm({ ...itemForm, marketValue: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
value={itemForm.name}
|
||||
onChange={(e) => setItemForm({ ...itemForm, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Rarity
|
||||
<select
|
||||
value={itemForm.rarity}
|
||||
onChange={(e) => setItemForm({ ...itemForm, rarity: e.target.value })}
|
||||
>
|
||||
{rarities.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Value (cents)
|
||||
<input
|
||||
type="number"
|
||||
value={itemForm.marketValue}
|
||||
onChange={(e) => setItemForm({ ...itemForm, marketValue: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Image
|
||||
<ImageField
|
||||
value={itemForm.imageUrl}
|
||||
onChange={(imageUrl) => setItemForm({ ...itemForm, imageUrl })}
|
||||
/>
|
||||
</label>
|
||||
<button className="btn" type="submit">
|
||||
Add
|
||||
Add item
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="admin-block">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="admin-row">
|
||||
<span>
|
||||
{item.name} · {item.rarity} · {formatCredits(item.marketValue)} cr
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Delete ${item.name}?`)) return;
|
||||
await api.deleteItem(item.id);
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="admin-block">
|
||||
{editingItemId === item.id && itemEdit ? (
|
||||
<form className="form" onSubmit={saveItemEdit}>
|
||||
<h3>Edit item #{item.id}</h3>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
value={itemEdit.name}
|
||||
onChange={(e) => setItemEdit({ ...itemEdit, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Rarity
|
||||
<select
|
||||
value={itemEdit.rarity}
|
||||
onChange={(e) => setItemEdit({ ...itemEdit, rarity: e.target.value })}
|
||||
>
|
||||
{rarities.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Value (cents)
|
||||
<input
|
||||
type="number"
|
||||
value={itemEdit.marketValue}
|
||||
onChange={(e) => setItemEdit({ ...itemEdit, marketValue: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Image
|
||||
<ImageField
|
||||
value={itemEdit.imageUrl}
|
||||
onChange={(imageUrl) => setItemEdit({ ...itemEdit, imageUrl })}
|
||||
/>
|
||||
</label>
|
||||
<div className="row-actions">
|
||||
<button className="btn" type="submit">
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary"
|
||||
onClick={() => {
|
||||
setEditingItemId(null);
|
||||
setItemEdit(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="admin-row">
|
||||
{item.imageUrl ? (
|
||||
<div className="admin-thumb">
|
||||
<img src={item.imageUrl} alt="" />
|
||||
</div>
|
||||
) : null}
|
||||
<span>
|
||||
{item.name} · {item.rarity} · {formatCredits(item.marketValue)} cr
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary"
|
||||
onClick={() => startEditItem(item)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Delete ${item.name}?`)) return;
|
||||
await api.deleteItem(item.id);
|
||||
await load();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -352,7 +607,10 @@ export default function Admin() {
|
||||
{c.items.length === 0 && <div className="muted">No drops yet.</div>}
|
||||
{c.items.map((ci) => (
|
||||
<div key={ci.id} className="drop-row">
|
||||
<span>
|
||||
<span className="drop-item-cell">
|
||||
{ci.item.imageUrl ? (
|
||||
<img src={ci.item.imageUrl} alt="" className="drop-thumb" />
|
||||
) : null}
|
||||
{ci.item.name} ({ci.item.rarity})
|
||||
</span>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user