diff --git a/packages/client/src/components/AdminPanel2.jsx b/packages/client/src/components/AdminPanel2.jsx
new file mode 100644
index 0000000..6ffb6f7
--- /dev/null
+++ b/packages/client/src/components/AdminPanel2.jsx
@@ -0,0 +1,244 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { admin as adminApi } from '../services/api.js';
+import { useAuth } from '../services/AuthContext.jsx';
+
+function Sidebar({ active, onNavigate, onBack }) {
+ const items = [
+ { id: 'dashboard', icon: '📊', label: 'Dashboard' },
+ { id: 'users', icon: '👥', label: 'Usuarios' },
+ { id: 'workshop', icon: '🎛', label: 'Workshop' },
+ ];
+ return (
+
+
+ {items.map(item => (
+
+ ))}
+
+
+
+ );
+}
+
+function DashboardView() {
+ const [stats, setStats] = useState(null);
+
+ useEffect(() => {
+ adminApi.stats().then(setStats).catch(() => {});
+ }, []);
+
+ if (!stats) return Cargando...
;
+
+ const kpis = [
+ { label: 'USUARIOS TOTALES', value: stats.users, color: 'var(--text)' },
+ { label: 'PATCHES COMPARTIDOS', value: stats.patches, color: 'var(--text)' },
+ { label: 'PREMIUM', value: stats.premium, color: 'var(--yellow)' },
+ { label: 'REPORTADOS', value: stats.flagged, color: 'var(--red)' },
+ ];
+
+ return (
+
+
Dashboard
+
+ {kpis.map(k => (
+
+ {k.label}
+ {k.value}
+
+ ))}
+
+
+ );
+}
+
+function UsersView() {
+ const [users, setUsers] = useState([]);
+ const [search, setSearch] = useState('');
+ const [filter, setFilter] = useState('');
+
+ const load = useCallback(async () => {
+ const params = new URLSearchParams();
+ if (search) params.set('q', search);
+ if (filter) params.set('role', filter);
+ const data = await adminApi.users(params.toString());
+ setUsers(data.users || []);
+ }, [search, filter]);
+
+ useEffect(() => { load(); }, [load]);
+
+ const changeRole = async (id, role) => {
+ await adminApi.updateUser(id, { role });
+ load();
+ };
+
+ return (
+
+
Usuarios
+
+
+ 🔍
+ setSearch(e.target.value)} />
+
+
+
+
+
+
+
+
+
+
+ USUARIO
+ EMAIL
+ ROL
+ REGISTRO
+ ACCIONES
+
+ {users.map(u => (
+
+
+
+ {u.username?.[0]?.toUpperCase()}
+
+
{u.username}
+
+
{u.email}
+
+
+ {u.role === 'premium' ? '★ ' : u.role === 'banned' ? '🚫 ' : ''}{u.role}
+
+
+
+ {new Date(u.createdAt).toLocaleDateString('es')}
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+function WorkshopModView() {
+ const [patches, setPatches] = useState([]);
+ const [filter, setFilter] = useState('');
+
+ const load = useCallback(async () => {
+ const params = new URLSearchParams();
+ if (filter === 'flagged') params.set('flagged', 'true');
+ if (filter === 'deleted') params.set('deleted', 'true');
+ const data = await adminApi.patches(params.toString());
+ setPatches(data.patches || []);
+ }, [filter]);
+
+ useEffect(() => { load(); }, [load]);
+
+ const moderate = async (id, action) => {
+ await adminApi.updatePatch(id, { action });
+ load();
+ };
+
+ return (
+
+
Workshop — Moderacion
+
+
+
+
+
+
+
+
+
+
+ PATCH
+ LIKES
+ ESTADO
+ ACCIONES
+
+ {patches.map(p => (
+
+
+
{p.title}
+ {p.tags?.length > 0 && (
+
+ {p.tags.map(t => {t})}
+
+ )}
+
+
♥ {p.likesCount}
+
+ {p.isDeleted
+ ? 🚫 Eliminado
+ : p.isFlagged
+ ? ⚠ Reportado
+ : ✓ Activo
+ }
+
+
+ {p.isDeleted ? (
+
+ ) : (
+ <>
+ {p.isFlagged && }
+
+ >
+ )}
+
+
+ ))}
+ {patches.length === 0 && (
+
No hay patches
+ )}
+
+
+ );
+}
+
+export default function AdminPanel2({ onBack }) {
+ const { isAdmin } = useAuth();
+ const [page, setPage] = useState('dashboard');
+
+ if (!isAdmin) {
+ return (
+
+
+
🔒
+
Acceso restringido a administradores
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {page === 'dashboard' && }
+ {page === 'users' && }
+ {page === 'workshop' && }
+
+
+ );
+}
diff --git a/packages/client/src/components/Workshop.jsx b/packages/client/src/components/Workshop.jsx
index f39abac..05b37bc 100644
--- a/packages/client/src/components/Workshop.jsx
+++ b/packages/client/src/components/Workshop.jsx
@@ -142,8 +142,8 @@ function PatchCard({ patch, onLoad, onLike }) {
);
}
-export default function Workshop({ onSwitchToSandbox, onSwitchToGame }) {
- const { isLoggedIn, openAuth, logout, user } = useAuth();
+export default function Workshop({ onSwitchToSandbox, onSwitchToGame, onSwitchToAdmin }) {
+ const { isLoggedIn, isAdmin, openAuth, logout, user } = useAuth();
const [patches, setPatches] = useState([]);
const [search, setSearch] = useState('');
const [activeTag, setActiveTag] = useState('');
@@ -209,6 +209,9 @@ export default function Workshop({ onSwitchToSandbox, onSwitchToGame }) {
+ {isAdmin && onSwitchToAdmin && (
+
+ )}
{isLoggedIn ? (
{user?.username?.[0]?.toUpperCase()}
diff --git a/packages/client/src/index.css b/packages/client/src/index.css
index cc1765a..6336088 100644
--- a/packages/client/src/index.css
+++ b/packages/client/src/index.css
@@ -1026,6 +1026,100 @@ input, textarea, [contenteditable] { -webkit-user-select: text; user-select: tex
.auth-card { padding: 24px 20px; max-height: 90vh; overflow-y: auto; }
}
+/* ===== Admin Panel v2 ===== */
+.adm-layout { display: flex; height: 100vh; background: var(--bg); }
+.adm-sidebar {
+ width: 220px; background: var(--panel); display: flex; flex-direction: column;
+ padding: 20px 16px; gap: 4px; flex-shrink: 0;
+}
+.adm-sidebar-logo { display: flex; align-items: center; gap: 8px; padding-bottom: 16px; }
+.adm-sidebar-item {
+ display: flex; align-items: center; gap: 10px; padding: 10px 12px;
+ border: none; border-radius: 6px; background: transparent; color: var(--text2);
+ font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit;
+ text-align: left; width: 100%; transition: all 0.15s;
+}
+.adm-sidebar-item:hover { color: var(--text); }
+.adm-sidebar-item.active { background: var(--surface); color: var(--accent); font-weight: 600; }
+
+.adm-main { flex: 1; padding: 24px 32px; overflow-y: auto; }
+.adm-page-title { font-size: 24px; font-weight: 700; color: var(--text); margin: 0 0 20px; }
+
+.adm-kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
+.adm-kpi-card {
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
+ padding: 20px; display: flex; flex-direction: column; gap: 4px;
+}
+.adm-kpi-label { font-size: 10px; font-weight: 700; color: var(--text2); letter-spacing: 1px; }
+.adm-kpi-value { font-size: 32px; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
+
+.adm-toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
+
+.adm-table {
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
+ overflow: hidden;
+}
+.adm-table-head {
+ display: flex; padding: 12px 20px; gap: 12px; align-items: center;
+ border-bottom: 1px solid var(--border);
+}
+.adm-table-head span {
+ font-size: 10px; font-weight: 700; color: var(--text2); letter-spacing: 1px;
+}
+.adm-table-row {
+ display: flex; padding: 10px 20px; gap: 12px; align-items: center;
+ border-bottom: 1px solid rgba(37,37,69,0.5);
+}
+.adm-table-row:last-child { border-bottom: none; }
+.adm-table-row.banned { opacity: 0.4; }
+
+.adm-col-grow { flex: 1; min-width: 0; }
+.adm-col-md { width: 200px; flex-shrink: 0; }
+.adm-col-sm { width: 120px; flex-shrink: 0; }
+.adm-col-xs { width: 100px; flex-shrink: 0; }
+.adm-text-muted { font-size: 12px; color: var(--text2); }
+
+.adm-user-cell { display: flex; align-items: center; gap: 8px; }
+.adm-user-cell span { font-size: 12px; font-weight: 500; color: var(--text); }
+
+.adm-role-badge {
+ display: inline-block; padding: 3px 8px; border-radius: 10px;
+ font-size: 10px; font-weight: 600;
+}
+.adm-role-badge.user { background: rgba(100,116,139,0.15); color: var(--text2); }
+.adm-role-badge.premium { background: rgba(255,204,0,0.15); color: var(--yellow); }
+.adm-role-badge.admin { background: rgba(0,229,255,0.15); color: var(--accent); }
+.adm-role-badge.banned { background: rgba(255,68,102,0.15); color: var(--red); }
+
+.adm-action-select {
+ padding: 4px 8px; background: var(--bg); border: 1px solid var(--border);
+ border-radius: 4px; color: var(--text); font-size: 11px; cursor: pointer;
+ font-family: inherit;
+}
+.adm-action-select:focus { outline: none; border-color: var(--accent); }
+
+.adm-actions { display: flex; gap: 6px; }
+.adm-act-btn {
+ padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border);
+ background: var(--bg); cursor: pointer; font-size: 10px; font-family: inherit;
+}
+.adm-act-btn.green { border-color: var(--green); color: var(--green); }
+.adm-act-btn.red { border-color: var(--red); color: var(--red); }
+
+@media (max-width: 768px) {
+ .adm-layout { flex-direction: column; }
+ .adm-sidebar { width: 100%; flex-direction: row; padding: 8px 12px; gap: 4px; overflow-x: auto; }
+ .adm-sidebar-logo { display: none; }
+ .adm-sidebar-item { white-space: nowrap; padding: 8px 12px; font-size: 12px; }
+ .adm-main { padding: 16px; }
+ .adm-kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
+ .adm-kpi-value { font-size: 24px; }
+ .adm-table-head { display: none; }
+ .adm-table-row { flex-wrap: wrap; gap: 6px; }
+ .adm-col-md, .adm-col-sm, .adm-col-xs { width: auto; flex-shrink: 1; }
+ .adm-col-grow { width: 100%; }
+}
+
/* ===== Fullscreen Keyboard ===== */
.keyboard-fullscreen {
position: fixed; inset: 0; z-index: 500;
diff --git a/packages/client/src/main.jsx b/packages/client/src/main.jsx
index fe2d268..11a324c 100644
--- a/packages/client/src/main.jsx
+++ b/packages/client/src/main.jsx
@@ -3,24 +3,27 @@ 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'
+ const [mode, setMode] = useState('game'); // 'game' | 'sandbox' | 'workshop' | 'admin'
const nav = {
toGame: () => setMode('game'),
toSandbox: () => setMode('sandbox'),
toWorkshop: () => setMode('workshop'),
+ toAdmin: () => setMode('admin'),
};
return (
{mode === 'sandbox' && }
{mode === 'game' && }
- {mode === 'workshop' && }
+ {mode === 'workshop' && }
+ {mode === 'admin' && }
);