import { useCallback, useEffect, useMemo, useState } from 'react'; import { api, formatCredits, compareCents, centsJson, toCentsBigInt } from '../api'; import ImageField from './ImageField'; const emptyItem = { name: '', rarity: 'MilSpec', category: 'Misc', marketValue: 100, imageUrl: '', }; function CategoryField({ value, onChange, categories }) { const options = value && !categories.includes(value) ? [...categories, value] : categories; return ( ); } export default function AdminItems() { const [items, setItems] = useState([]); const [rarities, setRarities] = useState([]); const [categories, setCategories] = useState([]); const [error, setError] = useState(''); const [query, setQuery] = useState(''); const [rarityFilter, setRarityFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); const [sort, setSort] = useState('category'); const [itemForm, setItemForm] = useState(emptyItem); const [editingId, setEditingId] = useState(null); const [edit, setEdit] = useState(null); const load = useCallback(async () => { const [i, r, c] = await Promise.all([ api.adminItems(), api.adminRarities(), api.adminCategories(), ]); setItems(i.items); setRarities(r.rarities); const names = (c.names || c.categories || []).map((entry) => typeof entry === 'string' ? entry : entry.name ); setCategories(names); setItemForm((f) => { const nextCategory = f.category && names.includes(f.category) ? f.category : names.includes('Misc') ? 'Misc' : names[0] || ''; return { ...f, rarity: f.rarity || r.rarities[0] || 'MilSpec', category: nextCategory, }; }); }, []); useEffect(() => { load().catch((err) => setError(err.message)); }, [load]); const usedCategories = useMemo(() => { const set = new Set(items.map((item) => item.category || 'Misc')); return [...set].sort((a, b) => a.localeCompare(b)); }, [items]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); let list = items.filter((item) => { const cat = item.category || 'Misc'; if (rarityFilter !== 'all' && item.rarity !== rarityFilter) return false; if (categoryFilter !== 'all' && cat !== categoryFilter) return false; if (!q) return true; return ( item.name.toLowerCase().includes(q) || item.rarity.toLowerCase().includes(q) || cat.toLowerCase().includes(q) ); }); list = [...list]; list.sort((a, b) => { if (sort === 'name') return a.name.localeCompare(b.name); if (sort === 'value') return compareCents(b.marketValue, a.marketValue); if (sort === 'rarity') return a.rarity.localeCompare(b.rarity); // category (default): group by category, then name const ca = (a.category || 'Misc').localeCompare(b.category || 'Misc'); if (ca !== 0) return ca; return a.name.localeCompare(b.name); }); return list; }, [items, query, rarityFilter, categoryFilter, sort]); return (

Items

{items.length} skins · categories + Field-Tested reference values

setQuery(e.target.value)} />
{error &&
{error}
}

Create item

{ e.preventDefault(); setError(''); try { await api.createItem({ ...itemForm, marketValue: centsJson(toCentsBigInt(itemForm.marketValue)), }); setItemForm({ ...emptyItem, rarity: rarities[0] || 'MilSpec', category: categories.includes('Misc') ? 'Misc' : categories[0] || '', }); await load(); } catch (err) { setError(err.message); } }} >

Showing {filtered.length} / {items.length}

{filtered.map((item) => (
{editingId === item.id && edit ? (
{ e.preventDefault(); setError(''); try { await api.updateItem(editingId, { name: edit.name, rarity: edit.rarity, category: edit.category, marketValue: centsJson(toCentsBigInt(edit.marketValue)), imageUrl: edit.imageUrl, }); setEditingId(null); setEdit(null); await load(); } catch (err) { setError(err.message); } }} >

Edit #{item.id}

) : (
{item.imageUrl ? (
) : (
?
)}
{item.name}
{item.category || 'Misc'} ·{' '} {item.rarity} · {formatCredits(item.marketValue)} cr
)}
))}
); }