diff --git a/src/admin/AdminApp.tsx b/src/admin/AdminApp.tsx index 1414f4d..51c9d81 100644 --- a/src/admin/AdminApp.tsx +++ b/src/admin/AdminApp.tsx @@ -5,7 +5,7 @@ import { AuthProvider } from "./context/AuthContext"; import { AlertProvider } from "./context/AlertContext"; import { queryClient } from "./lib/queryClient"; import ErrorBoundary from "./components/ErrorBoundary"; -import AdminLayout from "./components/AdminLayout"; +import AppShell from "./ui/AppShell"; import AlertContainer from "./components/AlertContainer"; import MuiProvider from "./ui/MuiProvider"; import Login from "./pages/Login"; @@ -14,7 +14,6 @@ import "./variables.css"; import "./base.css"; import "./forms.css"; import "./buttons.css"; -import "./layout.css"; import "./components.css"; import "./tables.css"; import "./datepicker.css"; @@ -104,7 +103,7 @@ export default function AdminApp() { {import.meta.env.DEV && UiKit && ( } /> )} - }> + }> } /> } /> } /> diff --git a/src/admin/components/AdminLayout.tsx b/src/admin/components/AdminLayout.tsx deleted file mode 100644 index 11009a6..0000000 --- a/src/admin/components/AdminLayout.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useState, useCallback } from "react"; -import { Outlet, Navigate, useLocation } from "react-router-dom"; -import { motion } from "framer-motion"; -import { useAuth } from "../context/AuthContext"; -import { useTheme } from "../../context/ThemeContext"; -import { setLogoutAlert } from "../utils/api"; -import useModalLock from "../hooks/useModalLock"; -import Sidebar from "./Sidebar"; -import ShortcutsHelp from "./ShortcutsHelp"; - -export default function AdminLayout() { - const { isAuthenticated, loading, user, logout } = useAuth(); - const { theme, toggleTheme } = useTheme(); - const [sidebarOpen, setSidebarOpen] = useState(false); - const [loggingOut, setLoggingOut] = useState(false); - const location = useLocation(); - - // Session is managed by AuthProvider (initial check + proactive refresh via setTimeout). - // Do not call checkSession on route changes — concurrent refresh calls with token rotation - // would invalidate each other and kick the user out. - - const handleLogout = useCallback(() => { - setLoggingOut(true); - setSidebarOpen(false); - setLogoutAlert(); - setTimeout(() => logout(), 400); - }, [logout]); - - useModalLock(sidebarOpen); - - if (loading) { - return ( -
-
-
-
-
- ); - } - - if (!isAuthenticated) { - return ; - } - - // If 2FA is required but user hasn't enabled it, redirect to dashboard (where setup lives) - const needs2FASetup = user?.require2FA && !user?.totpEnabled; - if (needs2FASetup && location.pathname !== "/") { - return ; - } - - return ( - - setSidebarOpen(false)} - onLogout={handleLogout} - /> - -
-
- - -
- - -
- -
- -
-
- -
- ); -} diff --git a/src/admin/components/Sidebar.tsx b/src/admin/components/Sidebar.tsx deleted file mode 100644 index 86702c9..0000000 --- a/src/admin/components/Sidebar.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { NavLink, useLocation } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; -import { useTheme } from "../../context/ThemeContext"; -import { menuSections, isItemActive, hasItemPermission } from "../ui/navData"; - -interface SidebarProps { - isOpen: boolean; - onClose: () => void; - onLogout: () => void; -} - -export default function Sidebar({ isOpen, onClose, onLogout }: SidebarProps) { - const { user, hasPermission } = useAuth(); - const { theme } = useTheme(); - const location = useLocation(); - - const visibleSections = menuSections - .map((section) => ({ - ...section, - items: section.items.filter((item) => - hasItemPermission(item, hasPermission), - ), - })) - .filter((section) => section.items.length > 0); - - return ( - <> -
- - - - ); -} diff --git a/src/admin/layout.css b/src/admin/layout.css deleted file mode 100644 index cbda43a..0000000 --- a/src/admin/layout.css +++ /dev/null @@ -1,582 +0,0 @@ -/* ============================================================================ - Layout - ============================================================================ */ - -.admin-layout { - display: flex; - min-height: 100vh; - min-height: 100dvh; - background: var(--bg-primary); -} - -@media (min-width: 1024px) { - .admin-layout { - align-items: flex-start; - } -} - -/* ============================================================================ - Sidebar - ============================================================================ */ - -/* -- Theme variables for sidebar -- */ -[data-theme="dark"] .admin-sidebar { - --sb-bg: #141414; - --sb-border: #2a2a2a; - --sb-text: #a0a0a0; - --sb-text-hover: #ddd; - --sb-hover-bg: #1f1f1f; - --sb-active-bg: #ffffff; - --sb-active-text: #141414; - --sb-label: #444; - --sb-muted: #555; - --sb-scrollbar: #333; -} - -[data-theme="light"] .admin-sidebar { - --sb-bg: #ffffff; - --sb-border: #e8e6e1; - --sb-text: #7c7c84; - --sb-text-hover: #1a1a1a; - --sb-hover-bg: #f5f4f2; - --sb-active-bg: #141414; - --sb-active-text: #ffffff; - --sb-label: #a0a0a0; - --sb-muted: #a0a0a0; - --sb-scrollbar: #ddd; -} - -.admin-sidebar { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100vh; - height: 100dvh; - z-index: 50; - background: var(--sb-bg); - border-right: 1px solid var(--sb-border); - display: flex; - flex-direction: column; - padding-top: env(safe-area-inset-top, 0px); - padding-bottom: env(safe-area-inset-bottom, 0px); - padding-left: env(safe-area-inset-left, 0px); - padding-right: env(safe-area-inset-right, 0px); - transform: translateX(-100%); - visibility: hidden; - transition: - transform 0.3s ease, - visibility 0.3s ease; - overflow: hidden; - overscroll-behavior: none; -} - -.admin-sidebar.open { - transform: translateX(0); - visibility: visible; - touch-action: none; -} - -@media (min-width: 1024px) { - .admin-sidebar { - right: auto; - width: 220px; - height: 100%; - transform: none; - visibility: visible; - padding: 0; - } -} - -[data-theme="light"] .admin-sidebar { - box-shadow: - 1px 0 0 0 var(--sb-border), - 4px 0 16px rgba(0, 0, 0, 0.04); -} - -/* Sidebar Overlay (mobile) */ -.admin-sidebar-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 49; - backdrop-filter: blur(2px); -} - -.admin-sidebar-overlay.open { - display: block; -} - -@media (min-width: 1024px) { - .admin-sidebar-overlay { - display: none !important; - } -} - -/* Sidebar Header */ -.admin-sidebar-header { - padding: 0 18px; - height: 73px; - border-bottom: 1px solid var(--sb-border); - display: flex; - align-items: center; - justify-content: space-between; - flex-shrink: 0; -} - -.admin-sidebar-logo { - height: 28px; - width: auto; -} - -.admin-sidebar-close { - display: block; - padding: 0.5rem; - background: transparent; - border: none; - color: var(--sb-text); - cursor: pointer; - border-radius: 6px; -} - -.admin-sidebar-close:hover { - background: var(--sb-hover-bg); - color: var(--sb-text-hover); -} - -@media (min-width: 1024px) { - .admin-sidebar-close { - display: none; - } -} - -/* Sidebar Navigation */ -.admin-sidebar-nav { - flex: 1; - min-height: 0; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; - padding: 4px 0; -} - -.admin-sidebar-nav::-webkit-scrollbar { - width: 5px; -} - -.admin-sidebar-nav::-webkit-scrollbar-track { - background: transparent; -} - -.admin-sidebar-nav::-webkit-scrollbar-thumb { - background: var(--sb-scrollbar); - border-radius: 99px; -} - -/* Nav Section */ -.admin-nav-section { - padding: 14px 10px 6px; -} - -.admin-nav-label { - font-size: 10px; - font-weight: 600; - letter-spacing: 1px; - text-transform: uppercase; - color: var(--sb-label); - padding: 0 8px; - margin-bottom: 4px; -} - -/* Nav Item */ -.admin-nav-item { - display: flex; - align-items: center; - gap: 9px; - padding: 7px 10px; - border-radius: 7px; - color: var(--sb-text); - cursor: pointer; - transition: all 0.15s ease; - font-size: 13px; - font-weight: 450; - margin-bottom: 1px; - text-decoration: none; - user-select: none; -} - -.admin-nav-item:hover { - background: var(--sb-hover-bg); - color: var(--sb-text-hover); -} - -.admin-nav-item.active { - background: var(--sb-active-bg); - color: var(--sb-active-text); - font-weight: 600; -} - -.admin-nav-item.active svg { - color: var(--accent-color); -} - -.admin-nav-item svg { - width: 16px; - height: 16px; - flex-shrink: 0; -} - -@media (max-width: 1023px) { - .admin-nav-item { - padding: 10px 12px; - font-size: 15px; - gap: 10px; - } - - .admin-nav-item svg { - width: 18px; - height: 18px; - } -} - -/* Sidebar Footer */ -.admin-sidebar-footer { - margin-top: auto; - border-top: 1px solid var(--sb-border); - padding: 14px 10px; - flex-shrink: 0; -} - -.admin-user-chip { - display: flex; - align-items: center; - gap: 10px; - padding: 8px; - border-radius: 8px; - margin-bottom: 4px; -} - -.admin-user-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--accent-color); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 12px; - flex-shrink: 0; -} - -.admin-user-details { - flex: 1; - min-width: 0; -} - -.admin-user-name { - color: var(--sb-text-hover); - font-size: 13px; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.admin-user-role { - color: var(--sb-muted); - font-size: 11px; -} - -.admin-logout-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 100%; - padding: 7px 10px; - background: transparent; - border: none; - color: var(--sb-text); - cursor: pointer; - border-radius: 7px; - font-size: 12px; - font-family: inherit; - transition: all 0.15s ease; -} - -.admin-logout-btn:hover { - background: var(--sb-hover-bg); - color: var(--sb-text-hover); -} - -.admin-logout-btn svg { - width: 16px; - height: 16px; - flex-shrink: 0; -} - -@media (max-width: 480px) { - .admin-sidebar-footer { - padding: 10px 8px; - } - - .admin-user-chip { - padding: 6px; - } - - .admin-logout-btn { - padding: 8px; - font-size: 13px; - } -} - -/* ============================================================================ - Main Content Area - ============================================================================ */ - -.admin-main { - flex: 1; - min-width: 0; - min-height: 100vh; - min-height: 100dvh; - display: flex; - flex-direction: column; -} - -@media (min-width: 1024px) { - .admin-main { - margin-left: 220px; - } -} - -/* Header */ -.admin-header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 30; - height: calc(73px + env(safe-area-inset-top, 0px)); - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 1rem; - padding-top: env(safe-area-inset-top, 0px); -} - -@media (min-width: 1024px) { - .admin-header { - left: 220px; - padding: 0 1.5rem; - } -} - -.admin-menu-btn { - display: block; - padding: 0.5rem; - background: transparent; - border: none; - color: var(--text-secondary); - cursor: pointer; - border-radius: var(--border-radius-sm); -} - -.admin-menu-btn:hover { - background: var(--bg-tertiary); -} - -@media (min-width: 1024px) { - .admin-menu-btn { - display: none; - } -} - -.admin-header-theme-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - padding: 0; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - color: var(--text-secondary); - cursor: pointer; - border-radius: 50%; - transition: var(--transition); - position: relative; - overflow: hidden; -} - -.admin-header-theme-btn:hover { - background: var(--bg-tertiary); - border-color: var(--border-color-hover); - transform: scale(1.05); -} - -.admin-theme-icon { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-primary); - transition: all 0.3s ease; - opacity: 0; - transform: rotate(180deg) scale(0.5); -} - -.admin-theme-icon.visible { - opacity: 1; - transform: rotate(0) scale(1); -} - -/* Content */ -.admin-content { - flex: 1; - padding: 1rem; - padding-top: calc(73px + 1rem + env(safe-area-inset-top, 0px)); -} - -@media (min-width: 1024px) { - .admin-content { - padding: 28px 32px; - padding-top: calc(73px + 28px); - } -} - -/* ============================================================================ - Page Headers - ============================================================================ */ - -.admin-page-header { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-bottom: 1rem; -} - -@media (min-width: 640px) { - .admin-page-header { - flex-direction: row; - align-items: center; - justify-content: space-between; - } -} - -.admin-page-title { - font-size: 22px; - font-weight: 700; - color: var(--text-primary); - font-family: var(--font-heading); - margin-bottom: 0.25rem; -} - -.admin-page-subtitle { - color: var(--text-secondary); - font-size: 13px; -} - -.admin-page-actions { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; -} - -@media (max-width: 640px) { - .admin-page-actions { - width: 100%; - } - - .admin-page-actions .admin-btn { - flex: 1; - } -} - -/* ============================================================================ - Grid System - ============================================================================ */ - -.admin-grid { - display: grid; - gap: 1rem; -} - -.admin-grid > .admin-card { - margin-bottom: 0; -} - -.admin-grid-3 { - grid-template-columns: 1fr; -} - -@media (min-width: 640px) { - .admin-grid-3 { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 1024px) { - .admin-grid-3 { - grid-template-columns: repeat(3, 1fr); - } -} - -.admin-grid-4 { - grid-template-columns: repeat(2, 1fr); -} - -@media (min-width: 768px) { - .admin-grid-4 { - grid-template-columns: repeat(4, 1fr); - } -} - -/* Page header on mobile */ -@media (max-width: 480px) { - .admin-page-title { - font-size: 18px; - } - - .admin-page-subtitle { - font-size: 12px; - } - - .admin-content { - padding: 12px !important; - padding-top: calc(73px + 12px + env(safe-area-inset-top, 0px)) !important; - } -} - -/* Grid - single column on small mobile */ -@media (max-width: 480px) { - .admin-grid-4 { - grid-template-columns: 1fr; - } -} - -/* ============================================================================ - Settings Grid - ============================================================================ */ - -.admin-settings-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5rem; -} - -.admin-settings-grid > .admin-card { - margin-bottom: 0; -} - -@media (max-width: 900px) { - .admin-settings-grid { - grid-template-columns: 1fr; - } -}