сделал кнопку настроек, вкладку настроек и сворачивание боковой панели

This commit is contained in:
Zuev
2026-03-27 15:03:52 +03:00
parent 7a2c385257
commit 6be8db0cd0
12 changed files with 859 additions and 52 deletions

View File

@@ -1,6 +0,0 @@
---
trigger: always_on
glob:
description:
---

View File

@@ -1,29 +0,0 @@
Контекст проекта:
Backend: Java 17, Spring Framework. Учитывай возможности этой версии языка и стандарты фреймворка.
Frontend: HTML, CSS, JavaScript.
Правила написания кода и комментариев:
Проверка: Перед написанием кода изучи проект, его структуру и используемые технологии. Не предлагай решения, которые не соответствуют текущей архитектуре или стеку.
Язык: Все комментарии и объяснения должны быть строго на русском языке.
Комментирование кода: Оставляй комментарии, объясняющие, за что отвечает та или иная часть кода. Перед крупными или смысловыми блоками обязательно ставь поясняющие метки (например: ``, /* таблица subjects */, // логика обработки subjects).
Обоснование решений: При написании нового кода кратко и максимально понятно объясняй, почему мы используем именно это решение, а не другое.
Современные подходы: На фронтенде используй самые современные и актуальные подходы (например, Flexbox, CSS Grid, семантические теги).
Правила работы с ошибками (Обучающий режим):
Если ты находишь ошибку в моем коде, не пиши сразу готовый исправленный код.
Дай мне точную подсказку, чтобы я мог сам найти и исправить баг (например: "У тебя не закрыт тег в 15 строке", "Ты забыл поставить аннотацию в контроллере Spring" или "Проверь отступы в таком-то классе"). Моя цель — научиться.
Правила работы с дизайном (UI/UX):
Перед добавлением новых стилей всегда сначала изучай, какие стили уже используются в проекте, чтобы сохранять единообразие.
Если ты видишь, что текущий дизайн откровенно плох, нелогичен или устарел — смело предлагай свои идеи по улучшению (цветовая палитра, отступы, шрифты). Я открыт к предложениям по улучшению визуала.

View File

@@ -754,3 +754,44 @@ tbody tr:hover {
align-items: center;
gap: 8px;
}
/* ===== Theme Toggle Button ===== */
.theme-toggle {
width: 42px;
height: 42px;
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: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
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);
}
.theme-toggle--fixed {
position: fixed;
top: 1.25rem;
right: 1.25rem;
}

View File

@@ -19,6 +19,27 @@
.sidebar-header {
padding: 1.25rem;
border-bottom: 1px solid var(--bg-card-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-close-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-radius: var(--radius-sm);
transition: all var(--transition);
}
.sidebar-close-btn:hover {
background: var(--bg-card-border);
color: var(--text-primary);
}
.logo {
@@ -99,7 +120,7 @@
border-top: 1px solid var(--bg-card-border);
}
.btn-logout {
.btn-settings {
width: 100%;
display: flex;
align-items: center;
@@ -116,16 +137,110 @@
position: relative;
}
.btn-logout:hover {
background: rgba(248, 113, 113, 0.1);
.btn-settings:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.settings-chevron {
margin-left: auto;
transition: transform 0.3s ease;
flex-shrink: 0;
}
.settings-dropdown.open .settings-chevron {
transform: rotate(180deg);
}
/* Settings Dropdown Menu */
.settings-dropdown {
position: relative;
}
.settings-menu {
position: absolute;
bottom: calc(100% + 0.5rem);
left: 0;
right: 0;
background: rgba(15, 23, 42, 0.97);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--bg-card-border);
border-radius: var(--radius-sm);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
padding: 0.4rem;
z-index: 200;
opacity: 0;
visibility: hidden;
transform: translateY(8px);
transition: opacity 0.25s ease, visibility 0.25s ease, transform 0.25s ease;
}
[data-theme="light"] .settings-menu {
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.1);
}
.settings-dropdown.open .settings-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.settings-menu-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.75rem;
border: none;
border-radius: 8px;
background: none;
color: var(--text-primary);
font-family: inherit;
font-size: 0.88rem;
cursor: pointer;
text-decoration: none;
transition: background 0.2s ease;
width: 100%;
}
.settings-menu-item:hover {
background: var(--bg-hover);
}
.settings-menu-item--danger {
color: var(--error);
}
.settings-menu-item--danger:hover {
background: rgba(248, 113, 113, 0.1);
}
.settings-menu-divider {
height: 1px;
background: var(--bg-card-border);
margin: 0.25rem 0.5rem;
}
/* ===== Main ===== */
.main {
flex: 1;
margin-left: 260px;
min-height: 100vh;
transition: margin-left 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* Desktop Collapse State */
@media (min-width: 769px) {
.sidebar.collapsed {
transform: translateX(-100%);
}
.main.sidebar-collapsed {
margin-left: 0;
}
.main.sidebar-collapsed .menu-toggle {
display: block;
}
}
.topbar {
@@ -180,7 +295,9 @@
backdrop-filter: blur(2px);
z-index: 9;
opacity: 0;
transition: opacity var(--transition);
visibility: hidden;
pointer-events: none;
transition: opacity var(--transition), visibility var(--transition);
}
/* ===== Responsive Mobile ===== */
@@ -212,5 +329,7 @@
.sidebar-overlay.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}

View File

@@ -36,6 +36,12 @@
</svg>
<span>Magistr</span>
</div>
<button class="sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item" data-tab="users">
@@ -126,8 +132,31 @@
</a>
</nav>
<div class="sidebar-footer">
<button class="btn-logout" id="btn-logout">
<div class="settings-dropdown" id="settings-dropdown">
<button class="btn-settings" id="btn-settings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
Настройки
<svg class="settings-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<div class="settings-menu" id="settings-menu">
<a href="/admin/settings/" class="settings-menu-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
Общие настройки
</a>
<div class="settings-menu-divider"></div>
<button class="settings-menu-item settings-menu-item--danger" id="btn-logout">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
@@ -136,6 +165,8 @@
Выйти
</button>
</div>
</div>
</div>
</aside>
<!-- Sidebar overlay (mobile) -->

View File

@@ -1,4 +1,7 @@
import './otel.js';
// OTel: загружаем только на продакшене (не на localhost)
if (!['localhost', '127.0.0.1'].includes(window.location.hostname)) {
import('./otel.js').catch(e => console.warn('OTel init skipped:', e.message));
}
import { isAuthenticatedAsAdmin } from './api.js';
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
@@ -37,7 +40,9 @@ const navItems = document.querySelectorAll('.nav-item[data-tab]');
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const menuToggle = document.getElementById('menu-toggle');
const sidebarCloseBtn = document.getElementById('sidebar-close-btn');
const btnLogout = document.getElementById('btn-logout');
const main = document.querySelector('.main');
// Initial auth check
if (!isAuthenticatedAsAdmin()) {
@@ -48,16 +53,56 @@ if (!isAuthenticatedAsAdmin()) {
applyRippleEffect();
closeAllDropdownsOnOutsideClick();
// Menu Toggle
// Init sidebar state from localStorage on load
if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'true') {
sidebar.classList.add('collapsed');
main.classList.add('sidebar-collapsed');
}
// Menu Toggle (Hamburger)
menuToggle.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('open');
} else {
sidebar.classList.remove('collapsed');
main.classList.remove('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', 'false');
}
});
// Sidebar Close (X button)
sidebarCloseBtn?.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
} else {
sidebar.classList.toggle('collapsed');
main.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
}
});
sidebarOverlay.addEventListener('click', () => {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
});
// Settings Dropdown
const settingsDropdown = document.getElementById('settings-dropdown');
const btnSettings = document.getElementById('btn-settings');
btnSettings.addEventListener('click', (e) => {
e.stopPropagation();
settingsDropdown.classList.toggle('open');
});
document.addEventListener('click', (e) => {
if (!settingsDropdown.contains(e.target)) {
settingsDropdown.classList.remove('open');
}
});
// Logout
btnLogout.addEventListener('click', () => {
localStorage.removeItem('token');

View File

@@ -0,0 +1,255 @@
/* ===== 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);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-close-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-radius: var(--radius-sm);
transition: all var(--transition);
}
.sidebar-close-btn:hover {
background: var(--bg-card-border);
color: var(--text-primary);
}
.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-back {
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;
text-decoration: none;
transition: background var(--transition), color var(--transition);
}
.btn-back:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* ===== Main ===== */
.main {
flex: 1;
margin-left: 260px;
min-height: 100vh;
transition: margin-left 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* Desktop Collapse State */
@media (min-width: 769px) {
.sidebar.collapsed {
transform: translateX(-100%);
}
.main.sidebar-collapsed {
margin-left: 0;
}
.main.sidebar-collapsed .menu-toggle {
display: block;
}
}
.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;
visibility: hidden;
pointer-events: none;
transition: opacity var(--transition), visibility 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;
visibility: visible;
pointer-events: auto;
}
}

View File

@@ -0,0 +1,153 @@
/* ===== Reset & Base ===== */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--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);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-placeholder: #475569;
--accent: #8b5cf6;
--accent-hover: #a78bfa;
--accent-glow: rgba(139, 92, 246, 0.4);
--accent-secondary: #ec4899;
--error: #ef4444;
--success: #10b981;
--warning: #f59e0b;
--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); }
}
/* ===== Theme Toggle ===== */
.theme-toggle {
width: 42px;
height: 42px;
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: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
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);
}
.theme-toggle--fixed {
position: fixed;
top: 1.25rem;
right: 1.25rem;
}
/* ===== Settings Placeholder ===== */
.settings-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 2rem;
animation: fadeIn 0.4s ease both;
}
.settings-placeholder .icon-wrap {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(236, 72, 153, 0.15));
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
box-shadow: 0 0 30px var(--accent-glow);
}
.settings-placeholder h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.settings-placeholder p {
color: var(--text-secondary);
font-size: 0.95rem;
max-width: 400px;
line-height: 1.6;
}

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Настройки — Magistr</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/layout.css">
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<svg width="32" height="32" viewBox="0 0 40 40" fill="none">
<rect width="40" height="40" rx="12" fill="url(#lg)" />
<path d="M12 20L18 26L28 14" stroke="#fff" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" />
<defs>
<linearGradient id="lg" x1="0" y1="0" x2="40" y2="40">
<stop stop-color="#6366f1" />
<stop offset="1" stop-color="#8b5cf6" />
</linearGradient>
</defs>
</svg>
<span>Настройки</span>
</div>
<button class="sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item" data-tab="general">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
Общие настройки
</a>
</nav>
<div class="sidebar-footer">
<a href="/admin/" class="btn-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12" />
<polyline points="12 19 5 12 12 5" />
</svg>
Назад в панель
</a>
</div>
</aside>
<!-- Sidebar overlay (mobile) -->
<div class="sidebar-overlay" id="sidebar-overlay"></div>
<!-- Main -->
<main class="main">
<header class="topbar">
<button class="menu-toggle" id="menu-toggle" aria-label="Меню">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<h1 id="page-title">Загрузка...</h1>
</header>
<section class="content" id="app-content">
<!-- Content loaded via JS -->
</section>
</main>
<script src="/theme-toggle.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,98 @@
// Settings page main.js
// Auth check
const token = localStorage.getItem('token');
const role = localStorage.getItem('role');
if (!token || role !== 'ADMIN') {
window.location.href = '/';
}
// Configuration
const ROUTES = {
general: { title: 'Общие настройки', file: 'views/general.html' },
};
let currentTab = null;
// DOM Elements
const appContent = document.getElementById('app-content');
const pageTitle = document.getElementById('page-title');
const navItems = document.querySelectorAll('.nav-item[data-tab]');
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const menuToggle = document.getElementById('menu-toggle');
const sidebarCloseBtn = document.getElementById('sidebar-close-btn');
const main = document.querySelector('.main');
// Init sidebar state from localStorage
if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'true') {
sidebar.classList.add('collapsed');
main.classList.add('sidebar-collapsed');
}
// Menu Toggle (Hamburger)
menuToggle.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('open');
} else {
sidebar.classList.remove('collapsed');
main.classList.remove('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', 'false');
}
});
// Sidebar Close (X button)
sidebarCloseBtn?.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
} else {
sidebar.classList.toggle('collapsed');
main.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
}
});
sidebarOverlay.addEventListener('click', () => {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
});
// Navigation
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const tab = item.dataset.tab;
switchTab(tab);
});
});
async function switchTab(tab) {
if (currentTab === tab || !ROUTES[tab]) return;
navItems.forEach(n => n.classList.remove('active'));
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
pageTitle.textContent = ROUTES[tab].title;
try {
appContent.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:2rem;">Загрузка...</div>';
const response = await fetch(ROUTES[tab].file);
if (!response.ok) throw new Error('Failed to load view');
const html = await response.text();
appContent.innerHTML = html;
currentTab = tab;
} catch (e) {
appContent.innerHTML = `<div style="padding:1rem;color:var(--error);">Ошибка загрузки: ${e.message}</div>`;
console.error(e);
}
// Close mobile menu if open
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
}
// Load default tab
switchTab('general');

View File

@@ -0,0 +1,11 @@
<div class="settings-placeholder">
<div class="icon-wrap">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
</div>
<h2>Общие настройки</h2>
<p>Этот раздел находится в разработке. Здесь будут доступны общие настройки системы.</p>
</div>