diff --git a/packages/client/src/App.jsx b/packages/client/src/App.jsx index b12a535..d7490e6 100644 --- a/packages/client/src/App.jsx +++ b/packages/client/src/App.jsx @@ -15,14 +15,14 @@ import { usePinchZoom } from './hooks/usePinchZoom.js'; import { getModulesByCategory } from './engine/moduleRegistry.js'; import { useAuth } from './services/AuthContext.jsx'; -export default function App({ onSwitchToGame, onSwitchToWorkshop }) { +export default function App({ onSwitchToGame, onSwitchToWorkshop, onSwitchToAdmin }) { const [, forceUpdate] = useState(0); const containerRef = useRef(null); const portPositions = useRef({}); const [tempWire, setTempWire] = useState(null); const connectingRef = useRef(null); const [presetModal, setPresetModal] = useState(null); - const { user, isLoggedIn, openAuth, logout } = useAuth(); + const { user, isLoggedIn, isAdmin, openAuth, logout } = useAuth(); const importRef = useRef(null); const isMobile = useIsMobile(); const [menuOpen, setMenuOpen] = useState(false); @@ -324,9 +324,15 @@ export default function App({ onSwitchToGame, onSwitchToWorkshop }) { )} {isLoggedIn ? ( -
-
{user.username?.[0]?.toUpperCase()}
- {user.username} +
+
+
{user.username?.[0]?.toUpperCase()}
+ {user.username} +
+
+ {isAdmin && onSwitchToAdmin && } + +
) : ( diff --git a/packages/client/src/components/AdminPanel2.jsx b/packages/client/src/components/AdminPanel2.jsx index 6ffb6f7..ea7065c 100644 --- a/packages/client/src/components/AdminPanel2.jsx +++ b/packages/client/src/components/AdminPanel2.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { admin as adminApi } from '../services/api.js'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { admin as adminApi, levels as levelsApi } from '../services/api.js'; import { useAuth } from '../services/AuthContext.jsx'; function Sidebar({ active, onNavigate, onBack }) { @@ -7,6 +7,7 @@ function Sidebar({ active, onNavigate, onBack }) { { id: 'dashboard', icon: '📊', label: 'Dashboard' }, { id: 'users', icon: '👥', label: 'Usuarios' }, { id: 'workshop', icon: '🎛', label: 'Workshop' }, + { id: 'levels', icon: '🎮', label: 'SynthQuest' }, ]; return (
@@ -215,6 +216,226 @@ function WorkshopModView() { ); } +function LevelsView() { + const [levels, setLevels] = useState([]); + const [editing, setEditing] = useState(null); // level being edited + const [showCreate, setShowCreate] = useState(false); + const fileRef = useRef(null); + + const load = useCallback(async () => { + try { + const data = await levelsApi.list(); + setLevels(data.levels || []); + } catch {} + }, []); + + useEffect(() => { load(); }, [load]); + + const handleCreate = async (form) => { + await levelsApi.create(form); + setShowCreate(false); + load(); + }; + + const handleUpdate = async (id, form) => { + await levelsApi.update(id, form); + setEditing(null); + load(); + }; + + const handleDelete = async (id) => { + if (!confirm('Eliminar este nivel?')) return; + await levelsApi.remove(id); + load(); + }; + + const handleImportPatch = async (levelId) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + const text = await file.text(); + try { + const data = JSON.parse(text); + await levelsApi.importPatch(levelId, { + modules: data.modules || [], + connections: data.connections || [], + }); + load(); + } catch (err) { + alert('Error importando: ' + err.message); + } + }; + input.click(); + }; + + return ( +
+
+

SynthQuest — Niveles

+ + {levels.length} custom + +
+ +
+ + {showCreate && ( + setShowCreate(false)} /> + )} + + {editing && ( + handleUpdate(editing.id, form)} onCancel={() => setEditing(null)} /> + )} + +
+
+ MUNDO + ID + TITULO + PATCH + ACCIONES +
+ {levels.map(lvl => ( +
+ + {lvl.worldId} + + + {lvl.levelId} + +
+ + {lvl.isBoss ? '👑 ' : ''}{lvl.title} + + {lvl.subtitle &&
{lvl.subtitle}
} +
+ + {lvl.preplacedData ? ( + + ✓ {lvl.preplacedData.modules?.length || 0} modules + + ) : ( + + )} + +
+ + +
+
+ ))} + {levels.length === 0 && ( +

+ No hay niveles custom. Los 96 niveles base estan hardcoded en el codigo. +

+ )} +
+
+ ); +} + +function LevelForm({ level, onSave, onCancel }) { + const [form, setForm] = useState({ + worldId: level?.worldId || 'w1', + levelId: level?.levelId || '', + title: level?.title || '', + subtitle: level?.subtitle || '', + description: level?.description || '', + concept: level?.concept || '', + availableModules: level?.availableModules || [], + isBoss: level?.isBoss || false, + sortOrder: level?.sortOrder || 0, + }); + const [modInput, setModInput] = useState(''); + + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + + const addMod = () => { + if (modInput.trim() && !form.availableModules.includes(modInput.trim())) { + set('availableModules', [...form.availableModules, modInput.trim()]); + setModInput(''); + } + }; + + const removeMod = (m) => set('availableModules', form.availableModules.filter(x => x !== m)); + + return ( +
+

+ {level ? 'Editar Nivel' : 'Nuevo Nivel'} +

+
+
+ + +
+
+ + set('levelId', e.target.value)} + placeholder="w1-9" disabled={!!level} /> +
+
+ + set('title', e.target.value)} /> +
+
+ + set('subtitle', e.target.value)} /> +
+
+ +