import { useEffect, useState } from 'react' type AdminItem = { id: number slug: string name: string slot: string rarity: string itemLevel: number healingPower: number maxResourceBonus: number glyph: string imageUrl: string description: string } type AdminEncounter = { id: number dungeonId: number sequence: number slug: string enemyName: string encounterType: string imageUrl: string } type AdminDifficulty = { id: number slug: string name: string droppedItemLevel: number } type AdminLootEntry = { encounterId: number itemId: number difficultyId: number dropWeight: number dropChance: number } type AdminRecipeComponent = { itemId: number quantity: number } type AdminRecipe = { id: number itemId: number difficultyId: number | null sourceDungeonId: number | null sourceEncounterId: number | null components: AdminRecipeComponent[] } type AdminDungeon = { id: number slug: string name: string } type AdminData = { items: AdminItem[] encounters: AdminEncounter[] difficulties: AdminDifficulty[] encounterLoot: AdminLootEntry[] craftingRecipes: AdminRecipe[] dungeons: AdminDungeon[] } const API = '/api/admin' async function fetchJson(url: string, init?: RequestInit): Promise { const res = await fetch(url, init) const body = await res.json() if (!res.ok) throw new Error(body.error ?? 'Request failed') return body } export function AdminScreen({ onBack }: { onBack: () => void }) { const [data, setData] = useState(null) const [tab, setTab] = useState<'items' | 'bosses' | 'loot' | 'crafting'>('items') const [error, setError] = useState('') const [saving, setSaving] = useState>({}) useEffect(() => { fetchJson(`${API}/data`) .then(setData) .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load')) }, []) if (error) return

{error}

if (!data) return

Loading admin data...

return (

Developer Tools

Admin Panel

{tab === 'items' && } {tab === 'bosses' && } {tab === 'loot' && } {tab === 'crafting' && }
) } function ItemsTab({ data, setData, setSaving, saving }: { data: AdminData | null setData: React.Dispatch> setSaving: (s: Record | ((prev: Record) => Record)) => void saving: Record }) { if (!data) return null const [filter, setFilter] = useState('') const [editId, setEditId] = useState(null) const [form, setForm] = useState>({}) const groups = groupBy(data.items.filter((i) => i.name.toLowerCase().includes(filter.toLowerCase()) || i.slug.includes(filter)), (i) => i.slot, ) async function saveItem(id: number) { setSaving((prev) => ({ ...prev, [`item-${id}`]: true })) try { const body: Record = {} if (form.name !== undefined) body.name = form.name if (form.glyph !== undefined) body.glyph = form.glyph if (form.description !== undefined) body.description = form.description if (form.rarity !== undefined) body.rarity = form.rarity if (form.slot !== undefined) body.slot = form.slot if (form.itemLevel !== undefined) body.item_level = form.itemLevel if (form.healingPower !== undefined) body.healing_power = form.healingPower if (form.maxResourceBonus !== undefined) body.max_resource_bonus = form.maxResourceBonus await fetchJson(`${API}/items/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) setData((prev) => prev ? { ...prev, items: prev.items.map((i) => i.id === id ? { ...i, ...form } : i), } : prev) setEditId(null) setForm({}) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Save failed') } finally { setSaving((prev) => ({ ...prev, [`item-${id}`]: false })) } } return (
setFilter(e.target.value)} /> {Object.entries(groups).map(([slot, items]) => (
{slot} ({items.length})
{items.map((item) => (
{editId === item.id ? (