SynthQuest admin: - New "🎮 SynthQuest" section in admin sidebar - List custom levels with world, ID, title, patch status - Create new level: world selector, title, subtitle, description, concept (hint), available modules (tag input), boss flag, sort order - Edit existing levels inline - Import patch base from sandbox JSON export (📥 button per level) - Delete levels with confirmation Server: - custom_levels table (PostgreSQL) - CRUD API at /api/v1/admin/levels - POST /:id/import-patch to import sandbox JSON as preplaced modules Admin access: - User badge is now a hover dropdown with "🛠 Admin" + "Cerrar sesion" - Admin visible in Sandbox toolbar, Workshop nav, and user dropdown - onSwitchToAdmin passed through navigation chain Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
53 lines
1.9 KiB
JavaScript
53 lines
1.9 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import App from './App';
|
|
import GameApp from './game/GameApp.jsx';
|
|
import Workshop from './components/Workshop.jsx';
|
|
import AdminPanel2 from './components/AdminPanel2.jsx';
|
|
import { AuthProvider } from './services/AuthContext.jsx';
|
|
import AuthModal from './components/AuthModal.jsx';
|
|
import './index.css';
|
|
|
|
function Root() {
|
|
const [mode, setMode] = useState('game'); // 'game' | 'sandbox' | 'workshop' | 'admin'
|
|
|
|
const nav = {
|
|
toGame: () => setMode('game'),
|
|
toSandbox: () => setMode('sandbox'),
|
|
toWorkshop: () => setMode('workshop'),
|
|
toAdmin: () => setMode('admin'),
|
|
};
|
|
|
|
return (
|
|
<AuthProvider>
|
|
{mode === 'sandbox' && <App onSwitchToGame={nav.toGame} onSwitchToWorkshop={nav.toWorkshop} onSwitchToAdmin={nav.toAdmin} />}
|
|
{mode === 'game' && <GameApp onSwitchToSandbox={nav.toSandbox} onSwitchToWorkshop={nav.toWorkshop} />}
|
|
{mode === 'workshop' && <Workshop onSwitchToSandbox={nav.toSandbox} onSwitchToGame={nav.toGame} onSwitchToAdmin={nav.toAdmin} />}
|
|
{mode === 'admin' && <AdminPanel2 onBack={nav.toGame} />}
|
|
<AuthModal />
|
|
</AuthProvider>
|
|
);
|
|
}
|
|
|
|
createRoot(document.getElementById('root')).render(<Root />);
|
|
|
|
// Configure and unlock audio context on first user interaction
|
|
import * as Tone from 'tone';
|
|
Tone.getContext().lookAhead = 0.05; // 50ms — tighter than default 100ms
|
|
const unlockAudio = () => {
|
|
if (Tone.context.state !== 'running') {
|
|
Tone.start().catch(() => {});
|
|
}
|
|
document.removeEventListener('pointerdown', unlockAudio);
|
|
document.removeEventListener('keydown', unlockAudio);
|
|
};
|
|
document.addEventListener('pointerdown', unlockAudio);
|
|
document.addEventListener('keydown', unlockAudio);
|
|
|
|
// Register service worker for PWA
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
});
|
|
}
|