import { useEffect, useState } from 'react' import type { CSSProperties, Dispatch, SetStateAction } 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 maxHealth: number baseDamage: number tankDamage: number partyDamage: number imageUrl: string description: 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 recommendedLevel: number contentType: string partySize: number experienceReward: number imageUrl: string description: string } type AdminUpgradePath = { fromItemId: number toItemId: number } type AdminAbility = { id: number classId: number slug: string name: string spellType: string cost: number cooldown: number power: number unlockLevel: number glyph: string description: string } type AdminTalent = { id: number classId: number slug: string name: string maxRank: number tier: number branch: number prerequisiteTalentId: number | null prerequisiteRank: number prerequisiteName: string | null effectType: string effectValuePerRank: number glyph: string description: string } type AdminClass = { id: number slug: string name: string resourceName: string maxResource: number themeColor: string description: string abilities: AdminAbility[] talents: AdminTalent[] } type AdminData = { items: AdminItem[] encounters: AdminEncounter[] difficulties: AdminDifficulty[] encounterLoot: AdminLootEntry[] craftingRecipes: AdminRecipe[] dungeons: AdminDungeon[] gearUpgradePaths: AdminUpgradePath[] classes: AdminClass[] } type AdminTab = 'items' | 'dungeons' | 'encounters' | 'loot' | 'crafting' | 'upgrades' | 'classes' type SavingState = Record type SetData = Dispatch> type SetSaving = Dispatch> const API = '/api/admin' const tabs: { id: AdminTab; label: string }[] = [ { id: 'items', label: 'Items' }, { id: 'dungeons', label: 'Dungeons' }, { id: 'encounters', label: 'Mobs/Bosses' }, { id: 'loot', label: 'Loot' }, { id: 'crafting', label: 'Crafting' }, { id: 'upgrades', label: 'Upgrades' }, { id: 'classes', label: 'Classes' }, ] 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') 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 === 'dungeons' && } {tab === 'encounters' && } {tab === 'loot' && } {tab === 'crafting' && } {tab === 'upgrades' && } {tab === 'classes' && }
) } function ItemsTab({ data, setData, setSaving, saving }: { data: AdminData setData: SetData setSaving: SetSaving saving: SavingState }) { const [filter, setFilter] = useState('') const [editId, setEditId] = useState(null) const [form, setForm] = useState>({}) const groups = groupBy(data.items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()) || item.slug.toLowerCase().includes(filter.toLowerCase()) || item.slot.toLowerCase().includes(filter.toLowerCase())), (item) => item.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.slug !== undefined) body.slug = form.slug 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}`, jsonRequest('PUT', body)) setData((prev) => prev ? { ...prev, items: prev.items.map((item) => item.id === id ? { ...item, ...form } : item) } : prev) setEditId(null) setForm({}) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Save failed') } finally { setSaving((prev) => ({ ...prev, [`item-${id}`]: false })) } } async function uploadItemImage(itemId: number, file: File | undefined) { if (!file) return setSaving((prev) => ({ ...prev, [`item-image-${itemId}`]: true })) try { const imageData = await fileToDataUrl(file) const result = await fetchJson<{ imageUrl: string }>(`${API}/items/${itemId}/image`, jsonRequest('PUT', { imageData })) setData((prev) => prev ? { ...prev, items: prev.items.map((item) => item.id === itemId ? { ...item, imageUrl: result.imageUrl } : item), } : prev) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Upload failed') } finally { setSaving((prev) => ({ ...prev, [`item-image-${itemId}`]: false })) } } return (
setFilter(e.target.value)} /> {Object.entries(groups).map(([slot, items]) => (
{slot} ({items.length})
{items.map((item) => (
{editId === item.id ? (