From 772b1107629bbe646919ea2f3fcfc5885c4f2e14 Mon Sep 17 00:00:00 2001 From: Zuev Date: Thu, 26 Feb 2026 00:27:10 +0300 Subject: [PATCH] Refactor admin frontend into modular SPA --- frontend/admin/admin.css | 1078 ------------------------- frontend/admin/admin.js | 911 --------------------- frontend/admin/css/components.css | 586 ++++++++++++++ frontend/admin/css/layout.css | 216 +++++ frontend/admin/css/main.css | 110 +++ frontend/admin/index.html | 373 +-------- frontend/admin/js/api.js | 57 ++ frontend/admin/js/main.js | 101 +++ frontend/admin/js/utils.js | 102 +++ frontend/admin/js/views/classrooms.js | 185 +++++ frontend/admin/js/views/edu-forms.js | 71 ++ frontend/admin/js/views/equipments.js | 88 ++ frontend/admin/js/views/groups.js | 108 +++ frontend/admin/js/views/subjects.js | 153 ++++ frontend/admin/js/views/users.js | 67 ++ frontend/admin/views/classrooms.html | 109 +++ frontend/admin/views/edu-forms.html | 34 + frontend/admin/views/equipments.html | 34 + frontend/admin/views/groups.html | 49 ++ frontend/admin/views/subjects.html | 76 ++ frontend/admin/views/users.html | 47 ++ 21 files changed, 2204 insertions(+), 2351 deletions(-) delete mode 100644 frontend/admin/admin.css delete mode 100644 frontend/admin/admin.js create mode 100644 frontend/admin/css/components.css create mode 100644 frontend/admin/css/layout.css create mode 100644 frontend/admin/css/main.css create mode 100644 frontend/admin/js/api.js create mode 100644 frontend/admin/js/main.js create mode 100644 frontend/admin/js/utils.js create mode 100644 frontend/admin/js/views/classrooms.js create mode 100644 frontend/admin/js/views/edu-forms.js create mode 100644 frontend/admin/js/views/equipments.js create mode 100644 frontend/admin/js/views/groups.js create mode 100644 frontend/admin/js/views/subjects.js create mode 100644 frontend/admin/js/views/users.js create mode 100644 frontend/admin/views/classrooms.html create mode 100644 frontend/admin/views/edu-forms.html create mode 100644 frontend/admin/views/equipments.html create mode 100644 frontend/admin/views/groups.html create mode 100644 frontend/admin/views/subjects.html create mode 100644 frontend/admin/views/users.html diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css deleted file mode 100644 index f178ab8..0000000 --- a/frontend/admin/admin.css +++ /dev/null @@ -1,1078 +0,0 @@ -/* ===== Reset & Base ===== */ -*, -*::before, -*::after { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - /* Deep dark premium background */ - --bg-primary: #0a0a0f; - --bg-sidebar: rgba(255, 255, 255, 0.02); - --bg-card: rgba(255, 255, 255, 0.03); - --bg-card-border: rgba(255, 255, 255, 0.05); - --bg-input: rgba(255, 255, 255, 0.04); - --bg-input-focus: rgba(255, 255, 255, 0.08); - --bg-hover: rgba(255, 255, 255, 0.06); - - /* Typography */ - --text-primary: #f8fafc; - --text-secondary: #94a3b8; - --text-placeholder: #475569; - - /* Vibrant Accents */ - --accent: #8b5cf6; - --accent-hover: #a78bfa; - --accent-glow: rgba(139, 92, 246, 0.4); - --accent-secondary: #ec4899; - - /* Status Colors */ - --error: #ef4444; - --success: #10b981; - --warning: #f59e0b; - - /* Spatial */ - --radius-sm: 10px; - --radius-md: 16px; - --transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); -} - -/* ===== Light Theme ===== */ -[data-theme="light"] { - --bg-primary: #f8fafc; - --bg-sidebar: rgba(255, 255, 255, 0.7); - --bg-card: rgba(255, 255, 255, 0.7); - --bg-card-border: rgba(0, 0, 0, 0.08); - --bg-input: rgba(0, 0, 0, 0.03); - --bg-input-focus: rgba(0, 0, 0, 0.06); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #0f172a; - --text-secondary: #475569; - --text-placeholder: #94a3b8; - --accent: #6366f1; - --accent-hover: #4f46e5; - --accent-glow: rgba(99, 102, 241, 0.3); - --accent-secondary: #d946ef; - --error: #ef4444; - --success: #10b981; - --warning: #f59e0b; -} - -[data-theme="light"] .form-group select option, -[data-theme="light"] .filter-row select option { - background: #fff; - color: #1a1a2e; -} - -[data-theme="light"] .nav-item.active { - background: rgba(99, 102, 241, 0.18); -} - -[data-theme="light"] .custom-multi-select .dropdown-menu { - background: rgba(255, 255, 255, 0.98); -} - -[data-theme="light"] .form-group input, -[data-theme="light"] .form-group select, -[data-theme="light"] .filter-row select { - border-color: rgba(0, 0, 0, 0.15); -} - -[data-theme="light"] tbody td { - border-bottom-color: rgba(0, 0, 0, 0.08); -} - -html { - font-size: 16px; - -webkit-font-smoothing: antialiased; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - min-height: 100vh; - display: flex; - transition: background 0.4s ease, color 0.4s ease; -} - -/* ===== Sidebar ===== */ -.sidebar { - width: 260px; - min-height: 100vh; - background: var(--bg-sidebar); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-right: 1px solid var(--bg-card-border); - display: flex; - flex-direction: column; - position: fixed; - left: 0; - top: 0; - bottom: 0; - z-index: 10; - transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); -} - -.sidebar-header { - padding: 1.25rem; - border-bottom: 1px solid var(--bg-card-border); -} - -.logo { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.15rem; - font-weight: 700; - letter-spacing: -0.02em; -} - -.sidebar-nav { - flex: 1; - padding: 0.75rem; -} - -.nav-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin-bottom: 0.25rem; - border-radius: var(--radius-sm); - color: var(--text-secondary); - text-decoration: none; - font-size: 0.95rem; - font-weight: 500; - transition: all var(--transition); - position: relative; - overflow: hidden; -} - -.nav-item::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--accent); - border-radius: 0 4px 4px 0; - transform: scaleY(0); - transition: transform var(--transition); - opacity: 0; -} - -.nav-item:hover { - background: var(--bg-hover); - color: var(--text-primary); - transform: translateX(4px); -} - -.nav-item.active { - background: rgba(139, 92, 246, 0.12); - color: var(--accent-hover); -} - -.nav-item.active::before { - transform: scaleY(1); - opacity: 1; -} - -.nav-item svg { - transition: transform var(--transition); -} - -.nav-item:hover svg, -.nav-item.active svg { - transform: scale(1.15) rotate(-5deg); -} - -/* Checkbox list styling */ -.checkbox-group { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding: 8px 0; -} - -.checkbox-item { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.9rem; - color: var(--text-base); - cursor: pointer; -} - -.checkbox-item input[type="checkbox"] { - cursor: pointer; - width: 16px; - height: 16px; - accent-color: var(--primary-color); -} - -/* Classroom Status Badges */ -.badge-available { - background-color: var(--success-bg); - color: var(--success-color); -} - -.badge-unavailable { - background-color: var(--error-bg); - color: var(--error-color); -} - -.status-cell { - display: flex; - align-items: center; - gap: 8px; -} - -.btn-icon-toggle { - background: var(--bg-body); - border: 1px solid var(--border-color); - color: var(--text-muted); - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - padding: 0; -} - -.btn-icon-toggle:hover { - background: var(--card-bg); - border-color: var(--primary-color); - color: var(--primary-color); - transform: rotate(45deg); - box-shadow: 0 0 10px rgba(99, 102, 241, 0.2); -} - -.btn-icon-toggle svg { - display: block; -} - -.nav-item.active { - background: rgba(99, 102, 241, 0.12); - color: var(--accent-hover); -} - -.sidebar-footer { - padding: 0.75rem; - border-top: 1px solid var(--bg-card-border); -} - -.btn-logout { - width: 100%; - display: flex; - align-items: center; - gap: 0.6rem; - padding: 0.65rem 0.8rem; - border: none; - border-radius: var(--radius-sm); - background: none; - color: var(--text-secondary); - font-family: inherit; - font-size: 0.9rem; - cursor: pointer; - transition: background var(--transition), color var(--transition); -} - -.btn-logout:hover { - background: rgba(248, 113, 113, 0.1); - color: var(--error); -} - -/* ===== Main ===== */ -.main { - flex: 1; - margin-left: 260px; - min-height: 100vh; -} - -.topbar { - padding: 1.5rem 2rem; - border-bottom: 1px solid var(--bg-card-border); - transition: border-color 0.4s ease; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.topbar h1 { - font-size: 1.3rem; - font-weight: 700; - letter-spacing: -0.02em; - flex: 1; -} - -.content { - padding: 1.5rem 2rem; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -/* ===== Cards ===== */ -@keyframes slideUpCard { - from { - opacity: 0; - transform: translateY(15px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.card { - background: var(--bg-card); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-md); - padding: 1.75rem; - position: relative; - overflow: visible; - transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); - animation: slideUpCard 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) both; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); -} - -.card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); - opacity: 0; - transition: opacity var(--transition); -} - -.card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1); - border-color: rgba(255, 255, 255, 0.12); -} - -.card:hover::before { - opacity: 1; -} - -/* Staggered cards */ -.card:nth-child(1) { - animation-delay: 0.1s; -} - -.card:nth-child(2) { - animation-delay: 0.2s; -} - -.card:nth-child(3) { - animation-delay: 0.3s; -} - -/* Specific Cards */ -.create-card { - z-index: 10; -} - -.card h2 { - font-size: 0.8rem; - font-weight: 600; - margin-bottom: 1rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -/* ===== Create Form ===== */ -.form-row { - display: flex; - gap: 1rem; - align-items: flex-end; - flex-wrap: wrap; -} - -.form-row .form-group { - flex: 1; - min-width: 160px; -} - -.form-group label { - display: block; - font-size: 0.78rem; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 0.4rem; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.form-group input, -.form-group select { - width: 100%; - padding: 0.75rem 1rem; - background: var(--bg-input); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.95rem; - outline: none; - transition: all var(--transition); -} - -.form-group input::placeholder { - color: var(--text-placeholder); - transition: opacity var(--transition); -} - -.form-group input:focus, -.form-group select:focus { - background: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 4px var(--accent-glow); - transform: translateY(-1px); -} - -.form-group input:focus::placeholder { - opacity: 0.5; -} - -/* Hide Number Arrows */ -input[type="number"]::-webkit-outer-spin-button, -input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -input[type="number"] { - -moz-appearance: textfield; -} - -.form-group select { - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - padding-right: 2.25rem; -} - -.form-group select option { - background: #1a1a2e; - color: var(--text-primary); -} - -.btn-create { - position: relative; - overflow: hidden; - padding: 0.75rem 1.75rem; - background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); - border: none; - border-radius: var(--radius-sm); - color: #fff; - font-family: inherit; - font-size: 0.95rem; - font-weight: 600; - letter-spacing: 0.02em; - cursor: pointer; - white-space: nowrap; - transition: all var(--transition); - box-shadow: 0 4px 15px var(--accent-glow); -} - -.btn-create::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(rgba(255, 255, 255, 0.2), transparent); - border-radius: inherit; - opacity: 0; - transition: opacity var(--transition); -} - -.btn-create:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px var(--accent-glow); -} - -.btn-create:hover::before { - opacity: 1; -} - -.btn-create:active { - transform: translateY(1px); - box-shadow: 0 2px 10px var(--accent-glow); -} - -@keyframes slideDownAlert { - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.form-alert { - display: none; - padding: 0.6rem 1rem; - border-radius: var(--radius-sm); - font-size: 0.85rem; - margin-top: 0.75rem; -} - -.form-alert.error { - display: block; - background: rgba(248, 113, 113, 0.1); - border: 1px solid rgba(248, 113, 113, 0.2); - color: var(--error); - animation: slideDownAlert 0.3s ease-out both; -} - -.form-alert.success { - display: block; - background: rgba(52, 211, 153, 0.1); - border: 1px solid rgba(52, 211, 153, 0.2); - color: var(--success); - animation: slideDownAlert 0.3s ease-out both; -} - -/* ===== Table ===== */ -.table-wrap { - overflow-x: visible; -} - -table { - width: 100%; - border-collapse: collapse; -} - -thead th { - text-align: left; - font-size: 0.78rem; - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; - padding: 0.6rem 0.8rem; - border-bottom: 1px solid var(--bg-card-border); -} - -tbody td { - padding: 0.85rem 1rem; - font-size: 0.95rem; - border-bottom: 1px solid var(--bg-card-border); - transition: background var(--transition); -} - -@keyframes slideInRow { - from { - opacity: 0; - transform: translateY(10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideInRow { - from { - opacity: 0; - transform: translateX(-10px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -tbody tr { - transition: background var(--transition); - animation: slideInRow 0.3s ease-out both; -} - -tbody tr:nth-child(1) { - animation-delay: 0.05s; -} - -tbody tr:nth-child(2) { - animation-delay: 0.1s; -} - -tbody tr:nth-child(3) { - animation-delay: 0.15s; -} - -tbody tr:nth-child(4) { - animation-delay: 0.2s; -} - -tbody tr:nth-child(5) { - animation-delay: 0.25s; -} - -tbody tr:nth-child(n+6) { - animation-delay: 0.3s; -} - -tbody tr:hover { - background: var(--bg-hover); -} - -.loading-row { - text-align: center; - color: var(--text-secondary); - padding: 2rem !important; -} - -/* ===== Role Badges ===== */ -.badge { - display: inline-block; - padding: 0.2rem 0.6rem; - border-radius: 999px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.badge-admin { - background: rgba(248, 113, 113, 0.15); - color: var(--error); -} - -.badge-teacher { - background: rgba(251, 191, 36, 0.15); - color: var(--warning); -} - -.badge-student { - background: rgba(52, 211, 153, 0.15); - color: var(--success); -} - -/* ===== Education Form Badge ===== */ -.badge-ef { - background: rgba(99, 102, 241, 0.15); - color: var(--accent-hover); -} - -/* ===== Card Header Row ===== */ -.card-header-row { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0.75rem; - margin-bottom: 1rem; -} - -.card-header-row h2 { - margin-bottom: 0; -} - -.filter-row { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.filter-row label { - font-size: 0.78rem; - font-weight: 500; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; - white-space: nowrap; -} - -.filter-row select { - padding: 0.45rem 2rem 0.45rem 0.7rem; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.85rem; - outline: none; - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.6rem center; - transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); -} - -.filter-row select:focus { - background-color: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); -} - -.filter-row select option { - background: #1a1a2e; - color: var(--text-primary); -} - -/* ===== Tab Content ===== */ -.tab-content { - animation: fadeIn 0.2s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(4px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ===== Delete Button ===== */ -.btn-delete { - padding: 0.35rem 0.7rem; - background: rgba(248, 113, 113, 0.1); - border: 1px solid rgba(248, 113, 113, 0.2); - border-radius: var(--radius-sm); - color: var(--error); - font-family: inherit; - font-size: 0.8rem; - cursor: pointer; - transition: background var(--transition), transform var(--transition); -} - -.btn-delete:hover { - background: rgba(248, 113, 113, 0.2); - transform: scale(1.05); -} - -/* ===== Ripple Effect ===== */ -.ripple { - position: absolute; - border-radius: 50%; - transform: scale(0); - animation: admin-ripple 0.6s linear; - background-color: rgba(255, 255, 255, 0.3); - pointer-events: none; -} - -@keyframes admin-ripple { - to { - transform: scale(4); - opacity: 0; - } -} - -/* ===== Mobile Menu Toggle ===== */ -.menu-toggle { - display: none; - padding: 0.4rem; - background: none; - border: none; - color: var(--text-primary); - cursor: pointer; - border-radius: var(--radius-sm); - transition: background var(--transition); -} - -.menu-toggle:hover { - background: var(--bg-hover); -} - -/* ===== Sidebar Overlay ===== */ -.sidebar-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 9; - backdrop-filter: blur(2px); -} - -/* ===== Theme Toggle Button ===== */ -.theme-toggle { - width: 40px; - height: 40px; - border: none; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - transition: all var(--transition); - z-index: 100; - flex-shrink: 0; -} - -.theme-toggle svg { - width: 20px; - height: 20px; - transition: transform 0.4s ease; -} - -.theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); -} - -.theme-toggle:active { - transform: scale(0.95); -} - -/* ===== Custom Multi Select ===== */ -.custom-multi-select { - position: relative; - width: 100%; -} - -.custom-multi-select .select-box { - width: 100%; - padding: 0.75rem 1rem; - background: var(--bg-input); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.95rem; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: all var(--transition); -} - -.custom-multi-select .select-box.active { - background: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 4px var(--accent-glow); -} - -.custom-multi-select .dropdown-icon { - transition: transform var(--transition); -} - -.custom-multi-select .select-box.active .dropdown-icon { - transform: rotate(180deg); -} - -.custom-multi-select .dropdown-menu { - position: absolute; - top: calc(100% + 5px); - left: 0; - width: 100%; - background: rgba(15, 15, 26, 0.98); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-md); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); - padding: 0.75rem; - z-index: 9999; - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all var(--transition); - max-height: 250px; - overflow-y: auto; -} - -.custom-multi-select .dropdown-menu.open { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.checkbox-group-vertical { - display: flex; - flex-direction: column; - gap: 8px; -} - -.checkbox-group-vertical .checkbox-item { - padding: 6px 8px; - border-radius: 6px; - transition: background var(--transition); - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; -} - -.checkbox-group-vertical .checkbox-item:hover { - background: var(--bg-hover); -} - -/* ===== Modals ===== */ -.modal-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - backdrop-filter: blur(2px); - align-items: center; - justify-content: center; - padding: 1rem; -} - -.modal-overlay.open { - display: flex; - animation: fadeIn 0.2s ease; -} - -.modal-content { - background: var(--bg-primary); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-md); - padding: 2rem; - width: 100%; - max-width: 600px; - position: relative; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); -} - -.modal-content h2 { - font-size: 1.2rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text-primary); - border-bottom: 1px solid var(--bg-card-border); - padding-bottom: 1rem; -} - -.modal-close { - position: absolute; - top: 1.5rem; - right: 1.5rem; - background: none; - border: none; - color: var(--text-secondary); - font-size: 1.5rem; - cursor: pointer; - line-height: 1; - transition: color var(--transition); -} - -.modal-close:hover { - color: var(--error); -} - -/* ===== Responsive ===== */ -@media (max-width: 768px) { - .sidebar { - width: 240px; - transform: translateX(-100%); - transition: transform 0.3s ease; - z-index: 20; - } - - .sidebar.open { - transform: translateX(0); - } - - .sidebar-overlay.open { - display: block; - } - - .menu-toggle { - display: flex; - } - - .main { - margin-left: 0; - } - - .topbar { - padding: 1rem; - display: flex; - align-items: center; - gap: 0.75rem; - } - - .topbar h1 { - font-size: 1.1rem; - } - - .content { - padding: 1rem; - } - - .card { - padding: 1rem; - } - - .form-row { - flex-direction: column; - align-items: stretch; - } - - .form-row .form-group { - min-width: 0; - } - - .btn-create { - width: 100%; - text-align: center; - } - - tbody td { - padding: 0.5rem 0.6rem; - font-size: 0.82rem; - } - - thead th { - padding: 0.5rem 0.6rem; - font-size: 0.72rem; - } - - .table-wrap { - margin: 0 -1rem; - padding: 0 1rem; - } -} - -@media (max-width: 480px) { - .topbar h1 { - font-size: 0.95rem; - } - - .badge { - font-size: 0.65rem; - padding: 0.15rem 0.45rem; - } - - .btn-delete { - padding: 0.25rem 0.5rem; - font-size: 0.72rem; - } -} \ No newline at end of file diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js deleted file mode 100644 index 9d0c01b..0000000 --- a/frontend/admin/admin.js +++ /dev/null @@ -1,911 +0,0 @@ -(() => { - 'use strict'; - - const token = localStorage.getItem('token'); - const role = localStorage.getItem('role'); - - if (!token || role !== 'ADMIN') { - window.location.href = '/'; - return; - } - - // ---- DOM refs ---- - const pageTitle = document.getElementById('page-title'); - const btnLogout = document.getElementById('btn-logout'); - const menuToggle = document.getElementById('menu-toggle'); - const sidebar = document.querySelector('.sidebar'); - const sidebarOverlay = document.getElementById('sidebar-overlay'); - - // Global Ripple Effect - document.addEventListener('click', function (e) { - const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout'); - if (!btn) return; - - const rect = btn.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const ripple = document.createElement('span'); - ripple.classList.add('ripple'); - ripple.style.left = `${x}px`; - ripple.style.top = `${y}px`; - - if (getComputedStyle(btn).position === 'static') { - btn.style.position = 'relative'; - } - btn.style.overflow = 'hidden'; - - btn.appendChild(ripple); - - setTimeout(() => ripple.remove(), 600); - }); - - // Users - const usersTbody = document.getElementById('users-tbody'); - const createForm = document.getElementById('create-form'); - const createAlert = document.getElementById('create-alert'); - - // Groups - const groupsTbody = document.getElementById('groups-tbody'); - const createGroupForm = document.getElementById('create-group-form'); - const createGroupAlert = document.getElementById('create-group-alert'); - const newGroupEfSelect = document.getElementById('new-group-ef'); - const filterEfSelect = document.getElementById('filter-ef'); - - // Education Forms - const efTbody = document.getElementById('ef-tbody'); - const createEfForm = document.getElementById('create-ef-form'); - const createEfAlert = document.getElementById('create-ef-alert'); - - // Classrooms - const classroomsTbody = document.getElementById('classrooms-tbody'); - const createClassroomForm = document.getElementById('create-classroom-form'); - const createClassroomAlert = document.getElementById('create-classroom-alert'); - const modalEditClassroom = document.getElementById('modal-edit-classroom'); - const modalEditClassroomClose = document.getElementById('modal-edit-classroom-close'); - const editClassroomForm = document.getElementById('edit-classroom-form'); - const editClassroomAlert = document.getElementById('edit-classroom-alert'); - const editEquipmentCheckboxes = document.getElementById('edit-equipment-checkboxes'); - - // Equipments - const equipmentsTbody = document.getElementById('equipments-tbody'); - const createEquipmentForm = document.getElementById('create-equipment-form'); - const createEquipmentAlert = document.getElementById('create-equipment-alert'); - const equipmentCheckboxes = document.getElementById('equipment-checkboxes'); - - // Subjects - const subjectsTbody = document.getElementById('subjects-tbody'); - const createSubjectForm = document.getElementById('create-subject-form'); - const createSubjectAlert = document.getElementById('create-subject-alert'); - const assignTeacherForm = document.getElementById('assign-teacher-form'); - const assignTeacherAlert = document.getElementById('assign-teacher-alert'); - const assignTeacherSelect = document.getElementById('assign-teacher-select'); - const assignSubjectSelect = document.getElementById('assign-subject-select'); - const teacherSubjectsTbody = document.getElementById('teacher-subjects-tbody'); - - // --- Multi-select logic --- - function updateSelectText(containerId, textId) { - const container = document.getElementById(containerId); - const textEl = document.getElementById(textId); - if (!container || !textEl) return; - const checked = Array.from(container.querySelectorAll('input:checked')); - if (checked.length === 0) { - textEl.textContent = 'Выберите оборудование...'; - } else if (checked.length === 1) { - textEl.textContent = checked[0].parentElement.textContent.trim(); - } else { - textEl.textContent = `Выбрано: ${checked.length}`; - } - } - - function initMultiSelect(boxId, menuId, textId, checkboxContainerId) { - const box = document.getElementById(boxId); - const menu = document.getElementById(menuId); - const container = document.getElementById(checkboxContainerId); - if (!box || !menu || !container) return; - - box.addEventListener('click', (e) => { - e.stopPropagation(); - const isOpen = menu.classList.contains('open'); - document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open')); - document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active')); - if (!isOpen) { - menu.classList.add('open'); - box.classList.add('active'); - } - }); - - menu.addEventListener('click', (e) => { - e.stopPropagation(); - }); - - container.addEventListener('change', () => { - updateSelectText(checkboxContainerId, textId); - }); - } - - initMultiSelect('equipment-select-box', 'equipment-dropdown-menu', 'equipment-select-text', 'equipment-checkboxes'); - initMultiSelect('edit-equipment-select-box', 'edit-equipment-dropdown-menu', 'edit-equipment-select-text', 'edit-equipment-checkboxes'); - - document.addEventListener('click', () => { - document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open')); - document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active')); - }); - // -------------------------- - - const navItems = document.querySelectorAll('.nav-item[data-tab]'); - const tabContents = document.querySelectorAll('.tab-content'); - - // ---- State ---- - let allGroups = []; - let allEducationForms = []; - let allEquipments = []; - let allSubjects = []; - let allTeachers = []; - - // ---- Tab Switching ---- - const TAB_TITLES = { - users: 'Управление пользователями', - groups: 'Управление группами', - 'edu-forms': 'Формы обучения', - equipments: 'Оборудование', - classrooms: 'Аудитории', - subjects: 'Дисциплины и преподаватели' - }; - - navItems.forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - switchTab(item.dataset.tab); - }); - }); - - function switchTab(tab) { - navItems.forEach(n => n.classList.remove('active')); - document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active'); - - tabContents.forEach(tc => tc.style.display = 'none'); - const target = document.getElementById('tab-' + tab); - if (target) target.style.display = ''; - - pageTitle.textContent = TAB_TITLES[tab] || ''; - - if (tab === 'users') loadUsers(); - if (tab === 'groups') { loadEducationForms().then(() => loadGroups()); } - if (tab === 'edu-forms') loadEducationForms(); - if (tab === 'equipments') loadEquipments(); - if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); } - if (tab === 'subjects') { Promise.all([loadSubjects(), loadTeachers()]).then(() => loadTeacherSubjects()); } - - sidebar.classList.remove('open'); - sidebarOverlay.classList.remove('open'); - } - - // ---- Mobile Menu ---- - menuToggle.addEventListener('click', () => { - sidebar.classList.toggle('open'); - sidebarOverlay.classList.toggle('open'); - }); - sidebarOverlay.addEventListener('click', () => { - sidebar.classList.remove('open'); - sidebarOverlay.classList.remove('open'); - }); - - // ---- Helpers ---- - const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' }; - const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' }; - - function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - - function showAlert(el, msg, type) { - el.className = 'form-alert ' + type; - el.textContent = msg; - } - - function hideAlert(el) { - el.className = 'form-alert'; - el.textContent = ''; - } - - // ============================================================ - // USERS - // ============================================================ - - async function loadUsers() { - try { - const res = await fetch('/api/users', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - const users = await res.json(); - renderUsers(users); - } catch (e) { - usersTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function renderUsers(users) { - if (!users.length) { - usersTbody.innerHTML = 'Нет пользователей'; - return; - } - usersTbody.innerHTML = users.map(u => ` - - ${u.id} - ${escapeHtml(u.username)} - ${ROLE_LABELS[u.role] || u.role} - - - `).join(''); - } - - createForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createAlert); - const username = document.getElementById('new-username').value.trim(); - const password = document.getElementById('new-password').value; - const role = document.getElementById('new-role').value; - if (!username || !password) { showAlert(createAlert, 'Заполните все поля', 'error'); return; } - - try { - const res = await fetch('/api/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ username, password, role }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createAlert, `Пользователь "${data.username}" создан`, 'success'); - createForm.reset(); - loadUsers(); - } else { - showAlert(createAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createAlert, 'Ошибка соединения', 'error'); } - }); - - usersTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить пользователя?')) return; - try { - const res = await fetch('/api/users/' + btn.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) loadUsers(); - else alert('Ошибка удаления'); - } catch (e) { alert('Ошибка соединения'); } - }); - - // ============================================================ - // EDUCATION FORMS - // ============================================================ - - async function loadEducationForms() { - try { - const res = await fetch('/api/education-forms', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - allEducationForms = await res.json(); - renderEfTable(allEducationForms); - populateEfSelects(allEducationForms); - } catch (e) { - efTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function renderEfTable(forms) { - if (!forms.length) { - efTbody.innerHTML = 'Нет форм обучения'; - return; - } - efTbody.innerHTML = forms.map(ef => ` - - ${ef.id} - ${escapeHtml(ef.name)} - - `).join(''); - } - - function populateEfSelects(forms) { - // Group creation select - const currentVal = newGroupEfSelect.value; - newGroupEfSelect.innerHTML = forms.map(ef => - `` - ).join(''); - if (currentVal && forms.find(f => f.id == currentVal)) { - newGroupEfSelect.value = currentVal; - } - - // Filter select - const currentFilter = filterEfSelect.value; - filterEfSelect.innerHTML = '' + - forms.map(ef => - `` - ).join(''); - if (currentFilter) filterEfSelect.value = currentFilter; - } - - createEfForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createEfAlert); - const name = document.getElementById('new-ef-name').value.trim(); - if (!name) { showAlert(createEfAlert, 'Введите название', 'error'); return; } - - try { - const res = await fetch('/api/education-forms', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createEfAlert, `Форма "${data.name}" создана`, 'success'); - createEfForm.reset(); - loadEducationForms(); - } else { - showAlert(createEfAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createEfAlert, 'Ошибка соединения', 'error'); } - }); - - efTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить форму обучения?')) return; - try { - const res = await fetch('/api/education-forms/' + btn.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) { - loadEducationForms(); - } else { - const data = await res.json(); - alert(data.message || 'Ошибка удаления'); - } - } catch (e) { alert('Ошибка соединения'); } - }); - - // ============================================================ - // GROUPS - // ============================================================ - - async function loadGroups() { - try { - const res = await fetch('/api/groups', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - allGroups = await res.json(); - applyGroupFilter(); - } catch (e) { - groupsTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function applyGroupFilter() { - const filterId = filterEfSelect.value; - const filtered = filterId - ? allGroups.filter(g => g.educationFormId == filterId) - : allGroups; - renderGroups(filtered); - } - - filterEfSelect.addEventListener('change', applyGroupFilter); - - function renderGroups(groups) { - if (!groups.length) { - groupsTbody.innerHTML = 'Нет групп'; - return; - } - groupsTbody.innerHTML = groups.map(g => ` - - ${g.id} - ${escapeHtml(g.name)} - ${escapeHtml(g.educationFormName)} - - `).join(''); - } - - createGroupForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createGroupAlert); - const name = document.getElementById('new-group-name').value.trim(); - const educationFormId = newGroupEfSelect.value; - if (!name) { showAlert(createGroupAlert, 'Введите название группы', 'error'); return; } - if (!educationFormId) { showAlert(createGroupAlert, 'Выберите форму обучения', 'error'); return; } - - try { - const res = await fetch('/api/groups', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name, educationFormId: Number(educationFormId) }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createGroupAlert, `Группа "${data.name}" создана`, 'success'); - createGroupForm.reset(); - loadGroups(); - } else { - showAlert(createGroupAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createGroupAlert, 'Ошибка соединения', 'error'); } - }); - - groupsTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить группу?')) return; - try { - const res = await fetch('/api/groups/' + btn.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) loadGroups(); - else alert('Ошибка удаления'); - } catch (e) { alert('Ошибка соединения'); } - }); - - // ============================================================ - // EQUIPMENTS - // ============================================================ - - async function loadEquipments() { - try { - const res = await fetch('/api/equipments', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - allEquipments = await res.json(); - renderEquipments(allEquipments); - renderEquipmentCheckboxes(allEquipments); - } catch (e) { - if (equipmentsTbody) equipmentsTbody.innerHTML = 'Ошибка загрузки'; - if (equipmentCheckboxes) equipmentCheckboxes.innerHTML = '

Ошибка загрузки

'; - } - } - - function renderEquipments(equipments) { - if (!equipments.length) { - equipmentsTbody.innerHTML = 'Нет оборудования'; - return; - } - equipmentsTbody.innerHTML = equipments.map(eq => ` - - ${eq.id} - ${escapeHtml(eq.name)} - - `).join(''); - } - - function renderEquipmentCheckboxes(equipments) { - if (!equipments.length) { - equipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; - return; - } - equipmentCheckboxes.innerHTML = equipments.map(eq => ` - - `).join(''); - updateSelectText('equipment-checkboxes', 'equipment-select-text'); - } - - createEquipmentForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createEquipmentAlert); - const name = document.getElementById('new-equipment-name').value.trim(); - if (!name) { showAlert(createEquipmentAlert, 'Введите название', 'error'); return; } - - try { - const res = await fetch('/api/equipments', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createEquipmentAlert, `Оборудование "${data.name}" добавлено`, 'success'); - createEquipmentForm.reset(); - loadEquipments(); - } else { - showAlert(createEquipmentAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createEquipmentAlert, 'Ошибка соединения', 'error'); } - }); - - equipmentsTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить оборудование?')) return; - try { - const res = await fetch('/api/equipments/' + btn.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) { - loadEquipments(); - } else { - const data = await res.json(); - alert(data.message || 'Ошибка удаления'); - } - } catch (e) { alert('Ошибка соединения'); } - }); - - // ============================================================ - // CLASSROOMS - // ============================================================ - - async function loadClassrooms() { - try { - const res = await fetch('/api/classrooms', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - const classrooms = await res.json(); - renderClassrooms(classrooms); - } catch (e) { - classroomsTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function renderClassrooms(classrooms) { - if (!classrooms.length) { - classroomsTbody.innerHTML = 'Нет аудиторий'; - return; - } - classroomsTbody.innerHTML = classrooms.map(c => { - const equipHtml = c.equipments && c.equipments.length - ? c.equipments.map(eq => escapeHtml(eq.name)).join(', ') - : '—'; - - return ` - - ${c.id} - ${escapeHtml(c.name)} - ${c.capacity} чел. - ${equipHtml} - -
- - ${c.isAvailable ? 'Доступна' : 'Не доступна'} - - -
- - - - - - `; - }).join(''); - } - - createClassroomForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createClassroomAlert); - const name = document.getElementById('new-classroom-name').value.trim(); - const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10); - - const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked')); - const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); - - if (!name || isNaN(capacity)) { showAlert(createClassroomAlert, 'Заполните обязательные поля', 'error'); return; } - - try { - const res = await fetch('/api/classrooms', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: true }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success'); - createClassroomForm.reset(); - updateSelectText('equipment-checkboxes', 'equipment-select-text'); - loadClassrooms(); - } else { - showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createClassroomAlert, 'Ошибка соединения', 'error'); } - }); - - classroomsTbody.addEventListener('click', async (e) => { - const btnDelete = e.target.closest('.btn-delete'); - const btnToggleStatus = e.target.closest('.btn-icon-toggle'); - const btnEdit = e.target.closest('.btn-edit-classroom'); - - if (btnDelete) { - if (!confirm('Удалить аудиторию?')) return; - try { - const res = await fetch('/api/classrooms/' + btnDelete.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) loadClassrooms(); - else alert('Ошибка удаления'); - } catch (err) { alert('Ошибка соединения'); } - } - - if (btnToggleStatus) { - const id = btnToggleStatus.dataset.id; - const currentStatus = btnToggleStatus.dataset.currentStatus === 'true'; - try { - const res = await fetch('/api/classrooms/' + id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ isAvailable: !currentStatus }), - }); - if (res.ok) loadClassrooms(); - else alert('Ошибка изменения статуса'); - } catch (err) { alert('Ошибка соединения'); } - } - - if (btnEdit) { - const id = btnEdit.dataset.id; - openEditClassroomModal(id); - } - }); - - let editingClassroomData = null; - - async function openEditClassroomModal(id) { - try { - const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token } }); - const classrooms = await res.json(); - editingClassroomData = classrooms.find(c => c.id == id); - - if (!editingClassroomData) return; - - document.getElementById('edit-classroom-id').value = editingClassroomData.id; - document.getElementById('edit-classroom-name').value = editingClassroomData.name; - document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity; - - if (allEquipments.length) { - editEquipmentCheckboxes.innerHTML = allEquipments.map(eq => { - const isChecked = editingClassroomData.equipments.some(e => e.id === eq.id) ? 'checked' : ''; - return ` - - `; - }).join(''); - } else { - editEquipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; - } - updateSelectText('edit-equipment-checkboxes', 'edit-equipment-select-text'); - - hideAlert(editClassroomAlert); - modalEditClassroom.classList.add('open'); - } catch (e) { - alert('Ошибка загрузки данных аудитории'); - } - } - - modalEditClassroomClose.addEventListener('click', () => { - modalEditClassroom.classList.remove('open'); - }); - - modalEditClassroom.addEventListener('click', (e) => { - if (e.target === modalEditClassroom) { - modalEditClassroom.classList.remove('open'); - } - }); - - editClassroomForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(editClassroomAlert); - const id = document.getElementById('edit-classroom-id').value; - const name = document.getElementById('edit-classroom-name').value.trim(); - const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10); - - const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked')); - const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); - - if (!name || isNaN(capacity)) { showAlert(editClassroomAlert, 'Заполните обязательные поля', 'error'); return; } - - try { - const res = await fetch('/api/classrooms/' + id, { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: editingClassroomData.isAvailable }), - }); - const data = await res.json(); - if (res.ok) { - modalEditClassroom.classList.remove('open'); - showAlert(createClassroomAlert, `Аудитория "${data.name}" обновлена`, 'success'); - loadClassrooms(); - } else { - showAlert(editClassroomAlert, data.message || 'Ошибка обновления', 'error'); - } - } catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); } - }); - - // ============================================================ - // SUBJECTS - // ============================================================ - - async function loadSubjects() { - try { - const res = await fetch('/api/subjects', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - allSubjects = await res.json(); - renderSubjects(allSubjects); - populateSubjectSelect(allSubjects); - } catch (e) { - if (subjectsTbody) subjectsTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function renderSubjects(subjects) { - if (!subjects.length) { - subjectsTbody.innerHTML = 'Нет дисциплин'; - return; - } - subjectsTbody.innerHTML = subjects.map(s => ` - - ${s.id} - ${escapeHtml(s.name)} - - `).join(''); - } - - function populateSubjectSelect(subjects) { - if (!assignSubjectSelect) return; - const currentVal = assignSubjectSelect.value; - assignSubjectSelect.innerHTML = '' + - subjects.map(s => ``).join(''); - if (currentVal && subjects.find(s => s.id == currentVal)) { - assignSubjectSelect.value = currentVal; - } - } - - async function loadTeachers() { - try { - const res = await fetch('/api/users/teachers', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - allTeachers = await res.json(); - populateTeacherSelect(allTeachers); - } catch (e) { - if (assignTeacherSelect) assignTeacherSelect.innerHTML = ''; - } - } - - function populateTeacherSelect(teachers) { - if (!assignTeacherSelect) return; - const currentVal = assignTeacherSelect.value; - if (!teachers.length) { - assignTeacherSelect.innerHTML = ''; - return; - } - assignTeacherSelect.innerHTML = '' + - teachers.map(t => ``).join(''); - if (currentVal && teachers.find(t => t.id == currentVal)) { - assignTeacherSelect.value = currentVal; - } - } - - async function loadTeacherSubjects() { - try { - const res = await fetch('/api/teacher-subjects', { - headers: { 'Authorization': 'Bearer ' + token }, - }); - const tsData = await res.json(); - renderTeacherSubjects(tsData); - } catch (e) { - if (teacherSubjectsTbody) teacherSubjectsTbody.innerHTML = 'Ошибка загрузки'; - } - } - - function renderTeacherSubjects(tsArray) { - if (!tsArray.length) { - teacherSubjectsTbody.innerHTML = 'Нет привязок'; - return; - } - teacherSubjectsTbody.innerHTML = tsArray.map(ts => ` - - ${escapeHtml(ts.username)} - ${escapeHtml(ts.subjectName)} - - `).join(''); - } - - createSubjectForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(createSubjectAlert); - const name = document.getElementById('new-subject-name').value.trim(); - if (!name) { showAlert(createSubjectAlert, 'Введите название', 'error'); return; } - - try { - const res = await fetch('/api/subjects', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ name }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(createSubjectAlert, `Дисциплина "${data.name}" добавлена`, 'success'); - createSubjectForm.reset(); - loadSubjects(); - } else { - showAlert(createSubjectAlert, data.message || 'Ошибка создания', 'error'); - } - } catch (e) { showAlert(createSubjectAlert, 'Ошибка соединения', 'error'); } - }); - - subjectsTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить дисциплину?')) return; - try { - const res = await fetch('/api/subjects/' + btn.dataset.id, { - method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + token }, - }); - if (res.ok) { - loadSubjects(); - loadTeacherSubjects(); - } else { - const data = await res.json(); - alert(data.message || 'Ошибка удаления'); - } - } catch (e) { alert('Ошибка соединения'); } - }); - - assignTeacherForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert(assignTeacherAlert); - const userId = assignTeacherSelect.value; - const subjectId = assignSubjectSelect.value; - if (!userId) { showAlert(assignTeacherAlert, 'Выберите преподавателя', 'error'); return; } - if (!subjectId) { showAlert(assignTeacherAlert, 'Выберите дисциплину', 'error'); return; } - - try { - const res = await fetch('/api/teacher-subjects', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ userId: Number(userId), subjectId: Number(subjectId) }), - }); - const data = await res.json(); - if (res.ok) { - showAlert(assignTeacherAlert, 'Привязка создана', 'success'); - loadTeacherSubjects(); - } else { - showAlert(assignTeacherAlert, data.message || 'Ошибка привязки', 'error'); - } - } catch (e) { showAlert(assignTeacherAlert, 'Ошибка соединения', 'error'); } - }); - - teacherSubjectsTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); - if (!btn) return; - if (!confirm('Удалить привязку?')) return; - try { - const res = await fetch('/api/teacher-subjects', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, - body: JSON.stringify({ userId: Number(btn.dataset.userId), subjectId: Number(btn.dataset.subjectId) }), - }); - if (res.ok) loadTeacherSubjects(); - else alert('Ошибка удаления'); - } catch (e) { alert('Ошибка соединения'); } - }); - - // ============================================================ - // LOGOUT & INIT - // ============================================================ - - btnLogout.addEventListener('click', () => { - localStorage.removeItem('token'); - localStorage.removeItem('role'); - window.location.href = '/'; - }); - - loadUsers(); -})(); diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css new file mode 100644 index 0000000..d03c9d2 --- /dev/null +++ b/frontend/admin/css/components.css @@ -0,0 +1,586 @@ +/* ===== Cards ===== */ +.card { + background: var(--bg-card); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-md); + padding: 1.75rem; + position: relative; + overflow: visible; + transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); + animation: slideUpCard 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) both; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + opacity: 0; + transition: opacity var(--transition); +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1); + border-color: rgba(255, 255, 255, 0.12); +} + +.card:hover::before { + opacity: 1; +} + +.create-card { + z-index: 10; +} + +.card h2 { + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* ===== Form Structure ===== */ +.form-row { + display: flex; + gap: 1rem; + align-items: flex-end; + flex-wrap: wrap; +} + +.form-group { + flex: 1; + min-width: 160px; +} + +.form-group label { + display: block; + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 0.4rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: inherit; + font-size: 0.95rem; + outline: none; + transition: all var(--transition); +} + +.form-group input::placeholder { + color: var(--text-placeholder); + transition: opacity var(--transition); +} + +.form-group input:focus, +.form-group select:focus { + background: var(--bg-input-focus); + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-glow); + transform: translateY(-1px); +} + +.form-group input:focus::placeholder { + opacity: 0.5; +} + +/* Hide Number Arrows */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +/* Select Base Style */ +.form-group select, +.filter-row select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + padding-right: 2.25rem; +} + +.form-group select option, +.filter-row select option { + background: #1a1a2e; + color: var(--text-primary); +} + +/* Light theme selects */ +[data-theme="light"] .form-group input, +[data-theme="light"] .form-group select, +[data-theme="light"] .filter-row select { + border-color: rgba(0, 0, 0, 0.15); +} + +[data-theme="light"] .form-group select option, +[data-theme="light"] .filter-row select option { + background: #fff; + color: #1a1a2e; +} + +/* Filter Row */ +.card-header-row { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.card-header-row h2 { + margin-bottom: 0; +} + +.filter-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.filter-row label { + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.filter-row select { + padding: 0.45rem 2rem 0.45rem 0.7rem; + background: var(--bg-input); + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 0.85rem; + transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); +} + +.filter-row select:focus { + background-color: var(--bg-input-focus); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + + +/* ===== Custom Multi-Select ===== */ +.custom-multi-select { + position: relative; + user-select: none; + width: 100%; +} + +.select-box { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 0.95rem; + cursor: pointer; + transition: all var(--transition); +} + +.select-box:hover { + background: var(--bg-hover); +} + +.select-box.active { + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-glow); +} + +.dropdown-icon { + transition: transform var(--transition); +} + +.select-box.active .dropdown-icon { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + width: 100%; + margin-top: 0.5rem; + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + padding: 1rem; + z-index: 100; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all var(--transition); +} + +[data-theme="light"] .custom-multi-select .dropdown-menu { + background: rgba(255, 255, 255, 0.98); +} + +.dropdown-menu.open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.checkbox-group-vertical { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 200px; + overflow-y: auto; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + font-size: 0.9rem; + color: var(--text-primary); + padding: 0.25rem 0; +} + +.checkbox-item input[type="checkbox"] { + cursor: pointer; + width: 1.1rem; + height: 1.1rem; + accent-color: var(--accent); +} + +/* ===== Buttons ===== */ +.btn-primary { + position: relative; + overflow: hidden; + padding: 0.75rem 1.75rem; + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-family: inherit; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + white-space: nowrap; + transition: all var(--transition); + box-shadow: 0 4px 15px var(--accent-glow); +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(rgba(255, 255, 255, 0.2), transparent); + border-radius: inherit; + opacity: 0; + transition: opacity var(--transition); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px var(--accent-glow); +} + +.btn-primary:hover::before { + opacity: 1; +} + +.btn-primary:active { + transform: translateY(1px); + box-shadow: 0 2px 10px var(--accent-glow); +} + +.btn-delete { + padding: 0.35rem 0.7rem; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.2); + border-radius: var(--radius-sm); + color: var(--error); + font-family: inherit; + font-size: 0.8rem; + cursor: pointer; + transition: background var(--transition), transform var(--transition); + position: relative; + overflow: hidden; +} + +.btn-delete:hover { + background: rgba(248, 113, 113, 0.2); + transform: scale(1.05); +} + +.btn-icon-toggle { + background: transparent; + border: 1px solid var(--bg-card-border); + color: var(--text-secondary); + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; +} + +.btn-icon-toggle:hover { + background: var(--bg-card); + border-color: var(--accent); + color: var(--accent); + transform: rotate(45deg); + box-shadow: 0 0 10px var(--accent-glow); +} + +.btn-edit-classroom { + padding: 0.35rem 0.7rem; + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: var(--radius-sm); + color: var(--accent-hover); + cursor: pointer; + transition: all var(--transition); +} + +.btn-edit-classroom:hover { + background: rgba(99, 102, 241, 0.2); + transform: translateY(-1px); +} + +/* ===== Alerts ===== */ +.form-alert { + display: none; + padding: 0.6rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.85rem; + margin-top: 0.75rem; +} + +.form-alert.error { + display: block; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.2); + color: var(--error); + animation: slideDownAlert 0.3s ease-out both; +} + +.form-alert.success { + display: block; + background: rgba(52, 211, 153, 0.1); + border: 1px solid rgba(52, 211, 153, 0.2); + color: var(--success); + animation: slideDownAlert 0.3s ease-out both; +} + +/* ===== Table ===== */ +.table-wrap { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead th { + text-align: left; + font-size: 0.78rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.6rem 0.8rem; + border-bottom: 1px solid var(--bg-card-border); +} + +tbody td { + padding: 0.85rem 1rem; + font-size: 0.95rem; + border-bottom: 1px solid var(--bg-card-border); + transition: background var(--transition); +} + +[data-theme="light"] tbody td { + border-bottom-color: rgba(0, 0, 0, 0.08); +} + +tbody tr { + transition: background var(--transition); + animation: slideInRow 0.3s ease-out both; +} + +/* Animation delays */ +tbody tr:nth-child(1) { + animation-delay: 0.05s; +} + +tbody tr:nth-child(2) { + animation-delay: 0.1s; +} + +tbody tr:nth-child(3) { + animation-delay: 0.15s; +} + +tbody tr:nth-child(4) { + animation-delay: 0.2s; +} + +tbody tr:nth-child(5) { + animation-delay: 0.25s; +} + +tbody tr:nth-child(n+6) { + animation-delay: 0.3s; +} + +tbody tr:hover { + background: var(--bg-hover); +} + +.loading-row { + text-align: center; + color: var(--text-secondary); + padding: 2rem !important; +} + +/* ===== Badges ===== */ +.badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.badge-admin { + background: rgba(248, 113, 113, 0.15); + color: var(--error); +} + +.badge-teacher { + background: rgba(251, 191, 36, 0.15); + color: var(--warning); +} + +.badge-student { + background: rgba(52, 211, 153, 0.15); + color: var(--success); +} + +.badge-ef { + background: rgba(99, 102, 241, 0.15); + color: var(--accent-hover); +} + +/* Classroom Status */ +.badge-available { + background: rgba(16, 185, 129, 0.15); + color: var(--success); +} + +.badge-unavailable { + background: rgba(248, 113, 113, 0.15); + color: var(--error); +} + +.status-cell { + display: flex; + align-items: center; + gap: 8px; +} + +/* ===== Modal ===== */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: 1000; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity var(--transition); +} + +.modal-overlay.open { + display: flex; + opacity: 1; +} + +.modal-content { + background: var(--bg-primary); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-md); + padding: 2rem; + width: 90%; + max-width: 500px; + position: relative; + transform: scale(0.95); + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); +} + +.modal-overlay.open .modal-content { + transform: scale(1); +} + +.modal-close { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + font-size: 1.5rem; + color: var(--text-secondary); + cursor: pointer; + transition: color var(--transition); +} + +.modal-close:hover { + color: var(--error); +} \ No newline at end of file diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css new file mode 100644 index 0000000..d6386ac --- /dev/null +++ b/frontend/admin/css/layout.css @@ -0,0 +1,216 @@ +/* ===== Sidebar ===== */ +.sidebar { + width: 260px; + min-height: 100vh; + background: var(--bg-sidebar); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-right: 1px solid var(--bg-card-border); + display: flex; + flex-direction: column; + position: fixed; + left: 0; + top: 0; + bottom: 0; + z-index: 10; + transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.sidebar-header { + padding: 1.25rem; + border-bottom: 1px solid var(--bg-card-border); +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 1.15rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.sidebar-nav { + flex: 1; + padding: 0.75rem; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin-bottom: 0.25rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.95rem; + font-weight: 500; + transition: all var(--transition); + position: relative; + overflow: hidden; +} + +.nav-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--accent); + border-radius: 0 4px 4px 0; + transform: scaleY(0); + transition: transform var(--transition); + opacity: 0; +} + +.nav-item:hover { + background: var(--bg-hover); + color: var(--text-primary); + transform: translateX(4px); +} + +.nav-item.active { + background: rgba(139, 92, 246, 0.12); + color: var(--accent-hover); +} + +[data-theme="light"] .nav-item.active { + background: rgba(99, 102, 241, 0.18); +} + +.nav-item.active::before { + transform: scaleY(1); + opacity: 1; +} + +.nav-item svg { + transition: transform var(--transition); +} + +.nav-item:hover svg, +.nav-item.active svg { + transform: scale(1.15) rotate(-5deg); +} + +.sidebar-footer { + padding: 0.75rem; + border-top: 1px solid var(--bg-card-border); +} + +.btn-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.65rem 0.8rem; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--text-secondary); + font-family: inherit; + font-size: 0.9rem; + cursor: pointer; + transition: background var(--transition), color var(--transition); + position: relative; +} + +.btn-logout:hover { + background: rgba(248, 113, 113, 0.1); + color: var(--error); +} + +/* ===== Main ===== */ +.main { + flex: 1; + margin-left: 260px; + min-height: 100vh; +} + +.topbar { + padding: 1.5rem 2rem; + border-bottom: 1px solid var(--bg-card-border); + transition: border-color 0.4s ease; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.topbar h1 { + font-size: 1.3rem; + font-weight: 700; + letter-spacing: -0.02em; + flex: 1; +} + +.content { + padding: 1.5rem 2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; + animation: fadeIn 0.2s ease; +} + +/* ===== Mobile Menu Toggle ===== */ +.menu-toggle { + display: none; + padding: 0.4rem; + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + border-radius: var(--radius-sm); + transition: background var(--transition); +} + +.menu-toggle:hover { + background: var(--bg-hover); +} + +.sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + z-index: 9; + opacity: 0; + transition: opacity var(--transition); +} + +/* ===== Responsive Mobile ===== */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + } + + .sidebar.open { + transform: translateX(0); + } + + .main { + margin-left: 0; + } + + .topbar { + padding: 1rem 1.25rem; + } + + .content { + padding: 1.25rem; + } + + .menu-toggle, + .sidebar-overlay { + display: block; + } + + .sidebar-overlay.open { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/admin/css/main.css b/frontend/admin/css/main.css new file mode 100644 index 0000000..a03d06e --- /dev/null +++ b/frontend/admin/css/main.css @@ -0,0 +1,110 @@ +/* ===== Reset & Base ===== */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* Deep dark premium background */ + --bg-primary: #0a0a0f; + --bg-sidebar: rgba(255, 255, 255, 0.02); + --bg-card: rgba(255, 255, 255, 0.03); + --bg-card-border: rgba(255, 255, 255, 0.05); + --bg-input: rgba(255, 255, 255, 0.04); + --bg-input-focus: rgba(255, 255, 255, 0.08); + --bg-hover: rgba(255, 255, 255, 0.06); + + /* Typography */ + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-placeholder: #475569; + + /* Vibrant Accents */ + --accent: #8b5cf6; + --accent-hover: #a78bfa; + --accent-glow: rgba(139, 92, 246, 0.4); + --accent-secondary: #ec4899; + + /* Status Colors */ + --error: #ef4444; + --success: #10b981; + --warning: #f59e0b; + + /* Spatial */ + --radius-sm: 10px; + --radius-md: 16px; + --transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +/* ===== Light Theme ===== */ +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-sidebar: rgba(255, 255, 255, 0.7); + --bg-card: rgba(255, 255, 255, 0.75); + --bg-card-border: rgba(0, 0, 0, 0.08); + --bg-input: rgba(0, 0, 0, 0.03); + --bg-input-focus: rgba(0, 0, 0, 0.06); + --bg-hover: rgba(0, 0, 0, 0.05); + --text-primary: #0f172a; + --text-secondary: #475569; + --text-placeholder: #94a3b8; + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-glow: rgba(99, 102, 241, 0.3); + --accent-secondary: #d946ef; + + --error: #ef4444; + --success: #10b981; + --warning: #f59e0b; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + display: flex; + transition: background 0.4s ease, color 0.4s ease; +} + +/* ===== Animations ===== */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideUpCard { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideDownAlert { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideInRow { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes admin-ripple { + to { transform: scale(4); opacity: 0; } +} + +.ripple { + position: absolute; + border-radius: 50%; + transform: scale(0); + animation: admin-ripple 0.6s linear; + background-color: rgba(255, 255, 255, 0.3); + pointer-events: none; +} diff --git a/frontend/admin/index.html b/frontend/admin/index.html index c8fa44a..ad7ac2f 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -8,7 +8,11 @@ - + + + + + @@ -31,7 +35,7 @@