Compare commits

...

7 Commits

23 changed files with 499 additions and 462 deletions

View File

@@ -16,12 +16,20 @@ body {
Цвет несёт смысл: акцент — действия и фокус, зелёный/янтарный — статусы защиты,
винный — деструктивные действия и негативная тональность. */
:root {
--bs-body-font-family: 'Inter', system-ui, -apple-system, sans-serif;
--bs-body-bg: #fafafa;
--bs-body-color: #131316;
--bs-border-color: #e6e6e9;
--bs-border-radius: 0.5rem;
--bs-border-radius-lg: 0.625rem;
--bs-link-color: #7a2333;
--bs-link-hover-color: #9d2b3c;
--c-bg: #fafafa;
--c-surface: #ffffff;
--c-text: #131316;
--c-text-muted: #74747c;
--c-text-link: #131316;
--c-text-link-hover: #7a2333;
--c-text-link: #7a2333;
--c-text-link-hover: #9d2b3c;
--c-border: #e6e6e9;
--c-border-strong: #d4d4da;
--c-accent: #7a2333;
@@ -47,8 +55,8 @@ body {
--c-surface: #17181d;
--c-text: #e8e9ec;
--c-text-muted: #9a9ba4;
--c-text-link: #e8e9ec;
--c-text-link-hover: #d98b98;
--c-text-link: #d98b98;
--c-text-link-hover: #f0b9c2;
--c-border: #26272e;
--c-border-strong: #34353d;
--c-accent: #a64d5f;
@@ -65,45 +73,26 @@ body {
--c-tone-neu: #9a9ba4;
}
a { color: var(--c-text-link); text-decoration: none; }
a:not(.btn):hover { color: var(--c-text-link-hover); }
a { color: var(--c-accent); text-decoration: none; }
a:not(.btn):hover { color: var(--c-accent-hover); }
/* Атрибут hidden должен скрывать элемент даже если у него display из класса */
[hidden] { display: none !important; }
/* Layout */
.page { display: flex; flex-direction: column; min-height: 100vh; }
.page-header { position: sticky; top: 0; z-index: 10; border-bottom: 1px solid var(--c-border); background: var(--c-surface); }
.page-header-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; gap: 0.5rem 1.75rem; flex-wrap: wrap; padding: 0.875rem 1.75rem; }
@media (max-width: 640px) {
.page-header-inner { padding: 0.5rem 0.875rem; gap: 0.5rem 1rem; }
.nav { gap: 0.5rem 0.75rem; flex-wrap: wrap; }
.user { gap: 0.5rem; }
.nav-user { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
}
.page-main { flex: 1; width: 100%; max-width: 1200px; margin: 0 auto; padding: 2.5rem 1.75rem 3.5rem; }
@media (max-width: 720px) { .page-main { padding: 1.5rem 0.875rem 2.5rem; } }
.page-footer { text-align: center; color: var(--c-text-muted); font-size: 0.75rem; padding: 1.5rem; }
/* Nav */
.nav { display: flex; align-items: center; gap: 1.125rem; flex: 1 1 auto; flex-wrap: wrap; font-size: 0.84375rem; }
.nav-link { color: var(--c-text-muted); text-decoration: none; font-weight: 500; }
.nav-link:hover { color: var(--c-text); }
.nav-link.active { color: var(--c-accent); }
.nav-sep { color: var(--c-text-muted); }
.logo { font-size: 1rem; font-weight: 700; color: var(--c-text); text-decoration: none; letter-spacing: -0.01em; }
.logo b { color: var(--c-accent); }
.user { display: flex; align-items: center; gap: 0.75rem; font-size: 0.84375rem; margin-left: auto; }
.nav-user { font-weight: 500; color: var(--c-text); }
.nav-user:hover { color: var(--c-accent); }
.nav-user.defense-active { color: var(--c-accent); }
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
.score-big { display: inline-block; margin-left: 0.5rem; font-size: 1.75rem; font-weight: 700; line-height: 1; color: var(--c-tone-pos); font-variant-numeric: tabular-nums; vertical-align: middle; }
.variant-name { font-weight: 600; }
.variant-name b { font-variant-numeric: tabular-nums; }
p { margin-top: 0; }
.theme-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.3rem 0.5rem; }
.theme-icon { width: 1.05rem; height: 1.05rem; display: block; }
.breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.breadcrumb .nav-sep { user-select: none; }
.check-list { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.25rem; max-height: 12rem; overflow-y: auto; }
@@ -139,6 +128,54 @@ p { margin-top: 0; }
.label { display: block; margin-bottom: 0.25rem; font-size: 0.875rem; font-weight: 500; }
.hint { margin-top: 0.25rem; font-size: 0.75rem; color: var(--c-text-muted); }
/* ===== B3-тема поверх Bootstrap: navbar, offcanvas, кнопки, ссылки ===== */
code { --bs-code-color: var(--c-text); color: var(--c-text); }
.b3-navbar { background: var(--c-surface); border-bottom: 1px solid var(--c-border); }
.b3-navbar .container, .page-main.container { max-width: 1200px; }
.b3-navbar .navbar-brand { color: var(--c-text); font-weight: 700; font-size: 1rem; }
.b3-navbar .navbar-brand b { color: var(--c-accent); }
.b3-navbar .navbar-toggler { border: 1px solid var(--c-border-strong); color: var(--c-accent); padding: 0.25rem 0.5rem; line-height: 1; }
.b3-navbar .navbar-toggler:focus { box-shadow: none; }
.nav-link { --bs-nav-link-color: var(--c-accent); --bs-nav-link-hover-color: var(--c-accent-hover); font-weight: 500; font-size: 0.875rem; }
.nav-link.active { color: var(--c-accent) !important; font-weight: 700; }
.navbar-close { border: 0; background: none; color: var(--c-text-muted); font-size: 1.1rem; line-height: 1; }
.navbar-close:hover { color: var(--c-text); }
.app-offcanvas { --bs-offcanvas-width: 17rem; --bs-offcanvas-bg: var(--c-surface); --bs-offcanvas-color: var(--c-text); --bs-offcanvas-border-color: var(--c-border); }
.app-offcanvas .offcanvas-header { border-bottom: 1px solid var(--c-border); justify-content: space-between; gap: 1rem; }
.app-offcanvas .navbar-nav { gap: 0.25rem; }
.app-offcanvas .nav-link { padding: 0.5rem 0.25rem; font-size: 1rem; }
@media (max-width: 767.98px) {
#mobileNav .offcanvas-body { display: flex; flex-direction: column; justify-content: space-between; gap: 1rem; }
}
.nav-user { font-weight: 500; color: var(--c-accent); text-decoration: none; }
.nav-user:hover { color: var(--c-accent-hover); }
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
.page-main { padding: 2.5rem 1.75rem 3.5rem; }
@media (max-width: 720px) { .page-main { padding: 1.5rem 0.875rem 2.5rem; } }
/* Кнопки: B3-палитра через переменные Bootstrap */
.btn {
--bs-btn-bg: var(--c-accent); --bs-btn-border-color: var(--c-accent); --bs-btn-color: #ffffff;
--bs-btn-hover-bg: var(--c-accent-hover); --bs-btn-hover-border-color: var(--c-accent-hover); --bs-btn-hover-color: #ffffff;
--bs-btn-active-bg: #6a1d2c; --bs-btn-active-border-color: #6a1d2c; --bs-btn-active-color: #ffffff;
--bs-btn-disabled-bg: var(--c-accent); --bs-btn-disabled-border-color: var(--c-accent); --bs-btn-disabled-color: #ffffff;
--bs-btn-font-size: 0.8125rem; --bs-btn-padding-y: 0.5rem; --bs-btn-padding-x: 1rem;
--bs-btn-border-radius: var(--radius);
}
.btn-primary { --bs-btn-bg: var(--c-accent); --bs-btn-border-color: var(--c-accent); --bs-btn-color: #ffffff;
--bs-btn-hover-bg: var(--c-accent-hover); --bs-btn-hover-border-color: var(--c-accent-hover); --bs-btn-hover-color: #ffffff;
--bs-btn-active-bg: #6a1d2c; --bs-btn-active-border-color: #6a1d2c; --bs-btn-active-color: #ffffff; }
.btn-secondary { --bs-btn-bg: var(--c-surface); --bs-btn-border-color: var(--c-border-strong); --bs-btn-color: var(--c-text);
--bs-btn-hover-bg: var(--c-bg); --bs-btn-hover-border-color: var(--c-border-strong); --bs-btn-hover-color: var(--c-text);
--bs-btn-active-bg: var(--c-bg); --bs-btn-active-border-color: var(--c-border-strong); --bs-btn-active-color: var(--c-text); }
.btn-danger { --bs-btn-bg: var(--c-surface); --bs-btn-border-color: #dcb9be; --bs-btn-color: var(--c-accent);
--bs-btn-hover-bg: var(--c-accent-soft); --bs-btn-hover-border-color: var(--c-accent); --bs-btn-hover-color: var(--c-accent);
--bs-btn-active-bg: var(--c-accent-soft); --bs-btn-active-border-color: var(--c-accent); --bs-btn-active-color: var(--c-accent); }
.btn-sm { --bs-btn-padding-y: 0.25rem; --bs-btn-padding-x: 0.6rem; --bs-btn-font-size: 0.75rem; }
a { --bs-link-color: var(--c-accent); --bs-link-hover-color: var(--c-accent-hover); text-decoration: none; }
/* Cards */
.card { border-radius: var(--radius-lg); background: var(--c-surface); border: 1px solid var(--c-border); }
.card-flush { border-radius: var(--radius-lg); background: var(--c-surface); border: 1px solid var(--c-border); padding: 0; overflow: hidden; }
@@ -163,56 +200,10 @@ p { margin-top: 0; }
.toolbar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
.stat-value { margin: 0.5rem 0 0; font-size: 1.875rem; font-weight: 600; }
.stat-title { font-size: 0.875rem; color: var(--c-text-muted); margin: 0; }
/* ---- Utilities: плоские классы поверх компонентов (в духе Bootstrap) ---- */
.flex { display: flex; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.m-0 { margin: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-auto { margin-left: auto; }
.p-0 { padding: 0; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.w-full { width: 100%; }
.w-260 { width: 260px; }
.overflow-x-auto { overflow-x: auto; }
.text-sm { font-size: 0.875rem; }
.text-muted { color: var(--c-text-muted); }
.text-strong { font-weight: 600; color: var(--c-text); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-break { overflow-wrap: anywhere; }
.font-mono { font-family: monospace; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.opacity-30 { opacity: 0.3; }
.offscreen { position: fixed; top: -1000px; left: 0; }
/* Badges — «этикетки»: строгая рамка, разрядка капсом; только индикаторы статуса */
.badge {
display: inline-flex; align-items: center;
border-radius: var(--radius-xs); padding: 0.1rem 0.5rem;
border-radius: var(--bs-btn-border-radius, var(--radius-sm)); padding: 0.5rem 1rem;
font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
border: 1px solid transparent;
}
@@ -243,7 +234,7 @@ tr:last-child td { border-bottom: none; }
.group-title { margin: 0 0 0.5rem; font-size: 1rem; font-weight: 600; }
.inline-form { display: inline-flex; align-items: center; gap: 0.375rem; margin: 0; }
/* Селект фокуса на лабу в тулбаре ведомости — по содержимому, не на всю ширину */
#lab-focus { width: auto; min-width: 11rem; }
.input-lab-focus { width: auto; min-width: 11rem; }
.input-score { width: 4.5rem; }
.inline-form .input-score, input[type="number"].input-score { width: 4.5rem; min-width: 4.5rem; flex: 0 0 auto; }
@@ -296,30 +287,30 @@ tr:last-child td { border-bottom: none; }
.confirm-text { margin: 0; font-size: 0.875rem; white-space: pre-line; overflow-wrap: anywhere; }
/* Modals */
dialog.modal {
dialog.dlg {
border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: 0;
background: var(--c-surface); color: var(--c-text);
width: min(34rem, calc(100vw - 2rem));
box-shadow: 0 16px 48px rgba(19, 19, 22, 0.28);
}
dialog.modal::backdrop { background: rgba(19, 19, 22, 0.5); }
dialog.dlg::backdrop { background: rgba(19, 19, 22, 0.5); }
/* Пока открыта модалка — страница не прокручивается, позиция скролла сохраняется;
прокрутка остаётся внутри .modal-body */
html.modal-open body {
position: fixed; left: 0; right: 0; width: 100%; top: var(--modal-top, 0);
padding-right: var(--modal-scrollbar, 0);
}
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-bottom: 1px solid var(--c-border); }
.modal-title { margin: 0; font-size: 1.0625rem; font-weight: 600; }
.modal-close { border: none; background: none; cursor: pointer; font-size: 1.25rem; color: var(--c-text-muted); padding: 0 0.25rem; }
.modal-close:hover { color: var(--c-text); }
.modal-body { padding: 1.5rem; max-height: 70vh; overflow-y: auto; display: flex; flex-direction: column; }
.dlg-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-bottom: 1px solid var(--c-border); }
.dlg-title { margin: 0; font-size: 1.0625rem; font-weight: 600; }
.dlg-close { border: none; background: none; cursor: pointer; font-size: 1.25rem; color: var(--c-text-muted); padding: 0 0.25rem; }
.dlg-close:hover { color: var(--c-text); }
.dlg-body { padding: 1.5rem; max-height: 70vh; overflow-y: auto; display: flex; flex-direction: column; }
/* Кнопки действий — внизу справа; при увеличенной модалке пустота снизу не остаётся */
.modal-body > form.form-stack:last-child { flex: 1 1 auto; }
.modal-body > form.form-stack:last-child > button:last-child,
.modal-body > form.form-stack:last-child > .flex:last-child,
.modal-body.form-stack > button:last-child,
.modal-body.form-stack > .flex:last-child,
.dlg-body > form.form-stack:last-child { flex: 1 1 auto; }
.dlg-body > form.form-stack:last-child > button:last-child,
.dlg-body > form.form-stack:last-child > .flex:last-child,
.dlg-body.form-stack > button:last-child,
.dlg-body.form-stack > .flex:last-child,
.modal-body > button:last-child,
.modal-body > .flex:last-child {
margin-top: auto;
@@ -342,7 +333,7 @@ html.modal-open body {
.md-body pre { background: var(--c-bg); border: 1px solid var(--c-border); border-radius: var(--radius); padding: 0.75rem 1rem; overflow-x: auto; font-size: 0.8125rem; }
.md-body code { font-family: monospace; }
/* Ссылки в учебных материалах — акцентные, с подчёркиванием: их видно в тексте */
.md-body a { color: var(--c-accent); text-decoration: underline; text-underline-offset: 2px; }
.md-body a { color: var(--c-accent); text-decoration: none; }
.md-body a:hover { color: var(--c-accent-hover); }
.md-body details.spoiler { border: 1px solid var(--c-border); border-radius: var(--radius); padding: 0.5rem 0.75rem; margin: 0.5rem 0; }
.md-body details.spoiler summary { cursor: pointer; font-weight: 500; color: var(--c-accent); }
@@ -477,8 +468,8 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.js-pub-count.is-hidden { display: none; }
/* Увеличенный диалог (оценка + комментарии) */
dialog.modal.modal-lg { width: min(72rem, calc(100vw - 2rem)); }
dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
dialog.dlg.dlg-lg { width: min(72rem, calc(100vw - 2rem)); }
dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
.mini-btn svg, .cell-btn svg { width: 1rem; height: 1rem; }
.chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11.5px; line-height: 1.5; }
@@ -490,7 +481,7 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
/* Материалы: ряды и ручки перетаскивания слева, одним столбцом */
.sort-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.sort-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sort-actions { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.sort-actions { margin-left: auto; display: flex; flex-wrap: nowrap; align-items: center; gap: 0.35rem; }
.sort-list { list-style: none; margin: 0; padding: 0; }
.sort-list > li { display: flex; align-items: center; gap: 0.5rem; min-width: 0; padding: 0.15rem 0; }
.chapter-card { padding: 0.75rem 1rem; }
@@ -504,12 +495,10 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
background: none; cursor: pointer; color: var(--c-text-muted);
}
.sub-toggle:hover { color: var(--c-text); }
.sub-arrow {
display: block; flex: 0 0 auto; width: 8px; height: 8px;
border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
transform: rotate(-135deg); transition: transform 0.2s;
}
.sub-acc.collapsed .sub-arrow { transform: rotate(45deg); }
.sub-arrow i { font-size: 1rem; line-height: 1; }
.sub-arrow { transform: rotate(0deg); transition: transform 0.2s; }
.sub-acc.collapsed .sub-arrow { transform: rotate(0deg); }
.sub-acc:not(.collapsed) .sub-arrow { transform: rotate(180deg); }
.section-body { padding-bottom: 0.4rem; }
/* ---- Матрица группы и карточки защиты ---- */
@@ -596,12 +585,8 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
.actions-col { display: inline-flex; flex-direction: column; gap: 0.35rem; align-items: stretch; min-width: 6.5rem; }
.actions-col .inline-form { display: block; width: 100%; }
.actions-col .btn { display: block; width: 100%; text-align: center; }
/* Монохромные SVG-иконки в ячейках матрицы (защита, оценка) */
.cell-ico {
width: 0.95rem; height: 0.95rem; display: block;
fill: none; stroke: currentColor; stroke-width: 2;
stroke-linecap: round; stroke-linejoin: round;
}
/* Иконки — SVG-файлы из /icons через CSS-маски: цвет наследуется от текста */
.cell-ico { font-size: 0.95rem; line-height: 1; }
/* tablekit: сортируемые таблицы и поиск */
th.th-sortable { cursor: pointer; user-select: none; }
@@ -682,10 +667,7 @@ th.th-filterable { padding-bottom: 0.35rem; }
.md-btn:hover { background: var(--c-surface); border-color: var(--c-border); }
.md-btn:active { transform: translateY(1px); }
.md-btn:focus-visible { outline: 2px solid var(--c-text-link); outline-offset: 1px; }
.md-ico {
width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.md-ico { font-size: 1.15rem; line-height: 1; }
.md-sep { width: 1px; align-self: stretch; margin: 0.2rem 0.3rem; background: var(--c-border); }
.md-editor-head {
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
@@ -703,3 +685,59 @@ th.th-filterable { padding-bottom: 0.35rem; }
}
.md-video-figure { margin: 1rem 0; text-align: center; }
.md-video { width: 100%; max-width: 100%; border-radius: var(--radius); background: #000; }
/* ---- Utilities: плоские классы поверх компонентов (в духе Bootstrap) ---- */
.flex { display: flex; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.m-0 { margin: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-auto { margin-left: auto; }
.p-0 { padding: 0; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.w-full { width: 100%; }
.w-260 { width: 260px; }
.overflow-x-auto { overflow-x: auto; }
.text-sm { font-size: 0.875rem; }
.text-muted { color: var(--c-text-muted); }
.text-strong { font-weight: 600; color: var(--c-text); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-break { overflow-wrap: anywhere; }
.font-mono { font-family: monospace; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.opacity-30 { opacity: 0.3; }
.offscreen { position: fixed; top: -1000px; left: 0; }
/* Материалы на мобильных: действия переносятся под заголовок */
@media (max-width: 720px) {
.sort-row, .sort-list > li { flex-wrap: wrap; row-gap: 0.35rem; }
.sort-row .sort-actions, .sort-list > li .sort-actions { margin-left: auto; flex-wrap: nowrap; justify-content: flex-end; }
.sort-row .sort-title, .sort-list > li .sort-title { flex: 1 1 auto; }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -166,7 +166,7 @@ function attachCommentAutocomplete(input) {
/* Если снизу внутри модалки мало места — раскрываем список вверх, чтобы он не обрезался. */
function positionList() {
var wrap = input.parentNode;
var scrollParent = input.closest('.modal-body') || document.documentElement;
var scrollParent = input.closest('.dlg-body') || document.documentElement;
var wr = wrap.getBoundingClientRect();
var sr = scrollParent.getBoundingClientRect();
var below = sr.bottom - wr.bottom;

View File

@@ -84,17 +84,20 @@
/* Активный пункт меню */
function initNavActive() {
var path = window.location.pathname;
var links = [].slice.call(document.querySelectorAll('#top-nav .nav-link'));
var best = null;
var links = [].slice.call(document.querySelectorAll('.b3-navbar .nav-link'));
var bestHref = null;
var bestLen = -1;
links.forEach(function (a) {
var href = a.getAttribute('href') || '';
if (href === '/') return;
if (path === href || path.indexOf(href + '/') === 0) {
if (href.length > bestLen) { best = a; bestLen = href.length; }
if (href.length > bestLen) { bestHref = href; bestLen = href.length; }
}
});
if (best) best.classList.add('active');
if (!bestHref) return;
links.forEach(function (a) {
if (a.getAttribute('href') === bestHref) a.classList.add('active');
});
}
/* Серверные уведомления: автоскрытие и кнопка закрытия */
@@ -199,7 +202,7 @@
document.addEventListener('click', function (e) {
var el = e.target;
var close = el.closest('.modal-close');
var close = el.closest('.dlg-close');
if (close) {
var dlg = close.closest('dialog');
if (dlg) dlg.close();

File diff suppressed because one or more lines are too long

View File

@@ -5,18 +5,18 @@
<div id="content">
<h1 class="page-title">Панель преподавателя</h1>
<div class="grid grid-stats">
<div class="card card-body"><p class="stat-title">Группы</p><p class="stat-value" th:text="${groupCount}">0</p></div>
<div class="card card-body"><p class="stat-title">Студенты</p><p class="stat-value" th:text="${studentCount}">0</p></div>
<div class="card card-body"><p class="stat-title">Лабораторные</p><p class="stat-value" th:text="${labCount}">0</p>
<p class="hint" th:if="${labCount > 0}">опубликовано: <span th:text="${publishedLabs}">0</span></p></div>
<div class="card card-body"><p class="stat-title">Средний балл</p>
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-3">
<div class="col"><div class="card card-body h-100"><p class="stat-title">Группы</p><p class="stat-value" th:text="${groupCount}">0</p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Студенты</p><p class="stat-value" th:text="${studentCount}">0</p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Лабораторные</p><p class="stat-value" th:text="${labCount}">0</p>
<p class="hint" th:if="${labCount > 0}">опубликовано: <span th:text="${publishedLabs}">0</span></p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Средний балл</p>
<p class="stat-value" th:if="${avgGrade != null}" th:text="${#numbers.formatDecimal(avgGrade, 1, 2)}">—</p>
<p class="stat-value text-muted" th:if="${avgGrade == null}">—</p></div>
<p class="stat-value text-muted" th:if="${avgGrade == null}">—</p></div></div>
</div>
<div class="grid grid-2 mt-8">
<div>
<div class="row g-4 mt-4">
<div class="col-lg-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title m-0">Лабораторные</h2>
<a th:href="@{/admin/labs}" class="text-sm">Все лабораторные →</a>
@@ -33,7 +33,7 @@
</table>
</div>
</div>
<div>
<div class="col-lg-6">
<div class="flex items-center justify-between mb-4">
<h2 class="section-title m-0">Группы</h2>
<a th:href="@{/admin/groups}" class="text-sm">Все группы →</a>

View File

@@ -16,8 +16,6 @@
</p>
</div>
<div class="actions">
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/submissions.zip(id=${group.id})}"
title="ZIP со всеми последними решениями группы по всем лабораторным">Скачать работы</a>
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
<button class="btn" type="button" data-modal="modal-add-student">+ Студент</button>
</div>
@@ -26,7 +24,7 @@
<div class="toolbar">
<input class="input table-search w-260" data-target="group-table"
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
<select class="input" id="lab-focus">
<select class="input input-lab-focus" id="lab-focus">
<option value="">все лабораторные</option>
<option th:each="lab : ${labs}" th:value="${lab.id}" th:text="|ЛР ${lab.number}|">—</option>
</select>
@@ -49,7 +47,7 @@
<button class="mini-btn" type="button"
th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|" data-action="assign"
title="Выдача вариантов: уровни, варианты, автокраспределение по группе">
<svg class="cell-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
<i class="cell-ico ico bi bi-list" aria-hidden="true"></i>
</button>
<form class="inline-form" th:with="pub=${labPublication[lab.id]}, pc=${pub != null ? pub.publishedCount : 0}, all=${pub != null and pub.allPublished}"
th:action="@{/admin/groups/{id}/publish(id=${group.id})}" method="post">
@@ -61,8 +59,8 @@
th:attr="data-lab=${lab.id},data-count=${pc},data-total=${pub != null ? pub.total : 0},data-all=${all}"
th:classappend="${all} ? 'is-all' : (${pc > 0} ? 'is-partial' : 'is-empty')"
th:title="${pub == null} ? '' : (${all} ? 'Оценки опубликованы всей группе — нажмите, чтобы скрыть у всех' : (${pc > 0} ? 'Опубликовано ' + ${pc} + ' из ' + ${pub.total} + ' — нажмите, чтобы опубликовать всем' : 'Показать оценки и комментарии всей группе'))">
<svg class="eye-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
<svg class="eye-closed" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg>
<i class="eye-open ico bi bi-eye" aria-hidden="true"></i>
<i class="eye-closed ico bi bi-eye-slash" aria-hidden="true"></i>
<span class="mini-count js-pub-count" th:classappend="(${all} or ${pc == 0}) ? 'is-hidden' : ''" th:text="${pc}"></span>
</button>
</form>
@@ -148,12 +146,12 @@
</div>
<!-- Диалог: оценка + комментарии -->
<dialog id="modal-grade" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-grade" class="dlg dlg-lg">
<div class="dlg-header">
<h2 class="dlg-title" id="grade-title">Оценка и комментарии</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="studentId" id="grade-student">
@@ -173,12 +171,12 @@
<!-- Диалог: индивидуальное задание (защита) -->
<!-- Диалог: назначение варианта -->
<dialog id="modal-variant" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title">Назначить вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-variant" class="dlg dlg-lg">
<div class="dlg-header">
<h2 class="dlg-title">Назначить вариант</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body form-stack">
<div class="dlg-body form-stack">
<p class="text-sm text-muted" id="variant-who">—</p>
<div><label class="label" for="variant-level">Уровень</label>
<select class="input" id="variant-level"></select></div>
@@ -209,12 +207,12 @@
</div>
</dialog>
<dialog id="modal-defense" class="modal">
<div class="modal-header">
<h2 class="modal-title">Индивидуальное задание</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-defense" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Индивидуальное задание</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="groupId" th:value="${group.id}">
@@ -257,12 +255,12 @@
<!-- Диалог добавления студента -->
<dialog id="modal-add-student" class="modal">
<div class="modal-header">
<h2 class="modal-title">Добавить студента</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-add-student" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Добавить студента</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" id="enroll-form" method="post"
th:action="@{/admin/groups/{gid}/students/enroll(gid=${group.id})}">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
@@ -278,20 +276,20 @@
</form>
<form class="form-stack mt-4" th:action="@{/admin/groups/{gid}/students/create(gid=${group.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div class="grid grid-2">
<div><label class="label" for="stNo">Студенческий номер (логин)</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="stNo">Студенческий номер (логин)</label>
<input class="input" id="stNo" name="studentNo" required></div>
<div><label class="label" for="stBasis">Основа обучения</label>
<div class="col-md-6"><label class="label" for="stBasis">Основа обучения</label>
<input class="input" id="stBasis" name="basis" placeholder="бюджет"></div>
</div>
</div>
<div><label class="label" for="stLast">Фамилия</label>
<input class="input" id="stLast" name="lastName" required></div>
<div class="grid grid-2">
<div><label class="label" for="stFirst">Имя</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="stFirst">Имя</label>
<input class="input" id="stFirst" name="firstName"></div>
<div><label class="label" for="stMiddle">Отчество</label>
<div class="col-md-6"><label class="label" for="stMiddle">Отчество</label>
<input class="input" id="stMiddle" name="middleName"></div>
</div>
</div>
<p class="hint">Новому студенту пароль будет сгенерирован автоматически.</p>
<button class="btn btn-primary" type="submit">Создать нового</button>
</form>
@@ -336,15 +334,15 @@
</div>
<!-- Большая модалка «Выдача вариантов»: содержимое подгружается фрагментом по лабе -->
<dialog id="modal-lab-assign" class="modal modal-lg">
<div class="modal-header">
<h2 class="modal-title" id="assign-modal-title">Выдача вариантов</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-lab-assign" class="dlg dlg-lg">
<div class="dlg-header">
<h2 class="dlg-title" id="assign-modal-title">Выдача вариантов</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body" id="assign-modal-body"></div>
<div class="dlg-body" id="assign-modal-body"></div>
</dialog>
<script th:src="@{/js/admin-group.js(v='11')}" defer></script>
<script th:src="@{/js/admin-group.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -27,12 +27,12 @@
</table>
</div>
<dialog id="modal-create-group" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая группа</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-create-group" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новая группа</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/groups}" method="post">
<div><label class="label" for="name">Название</label>
<input class="input" id="name" name="name" placeholder="ПИ-11БО" required autofocus></div>
@@ -42,12 +42,12 @@
</div>
</dialog>
<dialog id="modal-import-group" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт студентов</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-import-group" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Импорт студентов</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
<p class="text-sm text-muted">Файл .xls, .xlsx или .csv со столбцами: студенческий номер, ФИО, основа обучения. Логин — студенческий номер, пароль сгенерируется и покажется после импорта.</p>
<div><label class="label" for="file">Файл со списком</label>

View File

@@ -33,28 +33,25 @@
<h2 class="section-title m-0">Условие лабораторной</h2>
<button class="mini-btn" type="button" data-action="lab-edit"
title="Редактировать параметры лабораторной" aria-label="Редактировать параметры лабораторной">
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
</svg>
<i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i>
</button>
</div>
<div class="card card-body md-body mb-6" id="lab-description-view" th:utext="${descriptionHtml}"></div>
<form id="lab-edit-form" hidden class="card card-body form-stack mb-6" method="post"
th:action="@{/admin/labs/{id}/edit(id=${lab.id})}">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div class="grid grid-2">
<div><label class="label" for="l-title">Название</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="l-title">Название</label>
<input class="input" id="l-title" name="title" th:value="${lab.title}" required></div>
<div><label class="label" for="l-points">Максимум баллов</label>
<div class="col-md-6"><label class="label" for="l-points">Максимум баллов</label>
<input class="input" id="l-points" name="pointsMax" type="number" step="0.1" min="0"
th:value="${lab.pointsMax}" required></div>
</div>
<div class="grid grid-2">
<div><label class="label" for="l-deadline">Срок сдачи (время — МСК)</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="l-deadline">Срок сдачи (время — МСК)</label>
<input class="input" id="l-deadline" name="deadlineAt" type="datetime-local"
th:value="${deadlineLocal}"></div>
<div><label class="label" for="l-defense">Дата защиты (время — МСК)</label>
<div class="col-md-6"><label class="label" for="l-defense">Дата защиты (время — МСК)</label>
<input class="input" id="l-defense" name="defenseAt" type="datetime-local" th:value="${defenseLocal}">
</div>
</div>
@@ -85,10 +82,7 @@
<div class="flex items-center gap-2">
<button class="mini-btn" type="button" th:attr="data-modal=|modal-level-rename-${level.id}|"
title="Переименовать уровень" aria-label="Переименовать уровень">
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
</svg>
<i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i>
</button>
<form class="inline-form"
th:action="@{/admin/labs/{labId}/levels/{levelId}/delete(labId=${lab.id}, levelId=${level.id})}"
@@ -96,14 +90,7 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="mini-btn danger" type="submit" title="Удалить уровень"
aria-label="Удалить уровень">
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/>
<path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
<i class="cell-ico ico bi bi-trash" aria-hidden="true"></i>
</button>
</form>
<button class="btn btn-sm" type="button" th:attr="data-modal=|modal-variant-add-${level.id}|">+
@@ -121,10 +108,7 @@
<div class="flex gap-2">
<button class="mini-btn" type="button" th:attr="data-modal=|modal-variant-edit-${v.id}|"
title="Редактировать вариант" aria-label="Редактировать вариант">
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
</svg>
<i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i>
</button>
<form class="inline-form"
th:action="@{/admin/labs/{labId}/variants/{variantId}/delete(labId=${lab.id}, variantId=${v.id})}"
@@ -132,14 +116,7 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="mini-btn danger" type="submit" title="Удалить вариант"
aria-label="Удалить вариант">
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/>
<path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
<i class="cell-ico ico bi bi-trash" aria-hidden="true"></i>
</button>
</form>
</div>
@@ -151,12 +128,12 @@
</div>
<div th:each="level : ${lab.levels}" th:remove="tag">
<dialog th:id="|modal-level-rename-${level.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать уровень</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog th:id="|modal-level-rename-${level.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать уровень</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="levelId" th:value="${level.id}">
@@ -167,22 +144,22 @@
</form>
</div>
</dialog>
<dialog th:id="|modal-variant-add-${level.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title" th:text="|Новый вариант — ${level.title}|">Новый вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog th:id="|modal-variant-add-${level.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title" th:text="|Новый вариант — ${level.title}|">Новый вариант</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="labLevelId" th:value="${level.id}">
<div class="grid grid-2">
<div><label class="label" for="|va-number-${level.id}|">Номер</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="|va-number-${level.id}|">Номер</label>
<input class="input" th:id="|va-number-${level.id}|" name="number" required></div>
<div><label class="label" for="|va-title-${level.id}|">Название</label>
<div class="col-md-6"><label class="label" for="|va-title-${level.id}|">Название</label>
<input class="input" th:id="|va-title-${level.id}|" name="title"></div>
</div>
</div>
<div><label class="label" for="|va-statement-${level.id}|">Условие (markdown)</label>
<textarea class="input" th:id="|va-statement-${level.id}|" name="statementMd"
rows="8"></textarea></div>
@@ -191,25 +168,25 @@
</div>
</dialog>
<th:block th:each="v : ${level.variants}">
<dialog th:id="|modal-variant-edit-${v.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title" th:text="|Вариант ${v.number}|">Вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog th:id="|modal-variant-edit-${v.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title" th:text="|Вариант ${v.number}|">Вариант</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="variantId" th:value="${v.id}">
<input type="hidden" name="labLevelId" th:value="${level.id}">
<div class="grid grid-2">
<div><label class="label" for="|ve-number-${v.id}|">Номер</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="|ve-number-${v.id}|">Номер</label>
<input class="input" th:id="|ve-number-${v.id}|" name="number" th:value="${v.number}"
required></div>
<div><label class="label" for="|ve-title-${v.id}|">Название</label>
<div class="col-md-6"><label class="label" for="|ve-title-${v.id}|">Название</label>
<input class="input" th:id="|ve-title-${v.id}|" name="title" th:value="${v.title}">
</div>
</div>
</div>
<div><label class="label" for="|ve-statement-${v.id}|">Условие (markdown)</label>
<textarea class="input" th:id="|ve-statement-${v.id}|" name="statementMd" rows="10"
th:text="${v.statementMd}"></textarea></div>
@@ -219,7 +196,7 @@
</dialog>
</th:block>
</div>
<script th:src="@{/js/admin-lab.js(v='11')}" defer></script>
<script th:src="@{/js/admin-lab.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -49,12 +49,12 @@
</div>
<dialog id="modal-import-lab" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт лабораторной</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-import-lab" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Импорт лабораторной</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
<div><label class="label" for="labFile">Файл</label>
<input class="input" id="labFile" type="file" name="file" accept=".zip" required></div>
@@ -72,27 +72,27 @@
</div>
</dialog>
<dialog id="modal-create-lab" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая лабораторная</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-create-lab" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новая лабораторная</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/create}" method="post">
<div class="grid grid-2">
<div><label class="label" for="labNumber">Номер</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="labNumber">Номер</label>
<input class="input" id="labNumber" name="number" type="number" min="1" required></div>
<div><label class="label" for="labPoints">Максимум баллов</label>
<div class="col-md-6"><label class="label" for="labPoints">Максимум баллов</label>
<input class="input" id="labPoints" name="pointsMax" type="number" step="0.1" min="0" value="5" required></div>
</div>
</div>
<div><label class="label" for="labTitle">Название</label>
<input class="input" id="labTitle" name="title" required></div>
<div class="grid grid-2">
<div><label class="label" for="labDeadline">Срок сдачи</label>
<div class="row g-3">
<div class="col-md-6"><label class="label" for="labDeadline">Срок сдачи</label>
<input class="input" id="labDeadline" name="deadlineAt" type="datetime-local"></div>
<div><label class="label" for="labDefense">Дата защиты</label>
<div class="col-md-6"><label class="label" for="labDefense">Дата защиты</label>
<input class="input" id="labDefense" name="defenseAt" type="datetime-local"></div>
</div>
</div>
<div><label class="label" for="labStatus">Статус</label>
<select class="input" id="labStatus" name="status">
<option value="draft">draft</option>

View File

@@ -18,7 +18,6 @@
<tr>
<th class="th-sortable is-asc" title="ФИО — по возрастанию">ФИО</th>
<th>Логин</th>
<th>Номер</th>
<th>Группа</th>
<th class="text-right" data-nosort>Действия</th>
</tr>
@@ -27,7 +26,6 @@
<tr th:each="u : ${users}">
<td class="font-medium" th:text="${u.fullName}">—</td>
<td class="font-mono" th:text="${u.login}">—</td>
<td class="font-mono" th:text="${u.studentNo}">—</td>
<td><a th:if="${u.groupId}" th:href="@{/admin/groups/{id}(id=${u.groupId})}" th:text="${u.groupName}">—</a><span th:unless="${u.groupId}" class="text-muted">без группы</span></td>
<td class="text-right">
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post" data-confirm="Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.">
@@ -37,7 +35,7 @@
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(users)}"><td colspan="5" class="empty">Никого не нашли.</td></tr>
<tr th:if="${#lists.isEmpty(users)}"><td colspan="4" class="empty">Никого не нашли.</td></tr>
</tbody>
</table>
</div>

View File

@@ -15,66 +15,65 @@
</noscript>
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='2')}">
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
<link rel="stylesheet" th:href="@{/css/app.css(v='11')}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='11')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='11')}" disabled>
<script th:src="@{/js/highlight.min.js(v='11')}" defer></script>
<script th:src="@{/js/table-scroll.js(v='11')}" defer></script>
<script th:src="@{/js/tablekit.js(v='11')}" defer></script>
<script th:src="@{/js/theme-init.js(v='11')}"></script>
<script th:src="@{/js/app.js(v='11')}" defer></script>
<script th:src="@{/js/ui-state.js(v='11')}" defer></script>
<script th:src="@{/js/md-content.js(v='11')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='11')}" defer></script>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
<link rel="stylesheet" th:href="@{/css/app.css(v='19')}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='19')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='19')}" disabled>
<script th:src="@{/js/highlight.min.js(v='19')}" defer></script>
<script th:src="@{/js/table-scroll.js(v='19')}" defer></script>
<script th:src="@{/js/tablekit.js(v='19')}" defer></script>
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
<script th:src="@{/js/theme-init.js(v='19')}"></script>
<script th:src="@{/js/app.js(v='19')}" defer></script>
<script th:src="@{/js/ui-state.js(v='19')}" defer></script>
<script th:src="@{/js/md-content.js(v='19')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='19')}" defer></script>
</head>
<body>
<div class="page">
<header class="page-header">
<div class="page-header-inner">
<a th:href="@{/}" class="logo">LMS<b>.</b></a>
<nav class="nav" id="top-nav">
<th:block th:if="${currentUser != null}">
<a th:if="${currentUser.admin}" th:href="@{/admin/groups}" class="nav-link">Группы</a>
<a th:if="${currentUser.admin}" th:href="@{/admin/users}" class="nav-link">Пользователи</a>
<a th:if="${currentUser.admin}" th:href="@{/admin/labs}" class="nav-link">Лабораторные</a>
<a th:unless="${currentUser.admin}" th:href="@{/student/labs}" class="nav-link">Лабораторные</a>
<a th:href="@{/change-password}" class="nav-link">Смена пароля</a>
</th:block>
</nav>
<div class="user">
<th:block th:if="${currentUser != null}">
<a th:href="@{/admin}" th:if="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
<a th:href="@{/student}" th:unless="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
</th:block>
<button type="button" class="btn btn-secondary theme-btn"
aria-label="Сменить тему" title="Сменить тему">
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/>
<path d="M12 7 a5 5 0 0 1 0 10 z" fill="currentColor"/>
<g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
<line x1="12" y1="1.4" x2="12" y2="3.2"/>
<line x1="12" y1="20.8" x2="12" y2="22.6"/>
<line x1="1.4" y1="12" x2="3.2" y2="12"/>
<line x1="20.8" y1="12" x2="22.6" y2="12"/>
<line x1="4.5" y1="4.5" x2="5.8" y2="5.8"/>
<line x1="18.2" y1="18.2" x2="19.5" y2="19.5"/>
<line x1="4.5" y1="19.5" x2="5.8" y2="18.2"/>
<line x1="18.2" y1="5.8" x2="19.5" y2="4.5"/>
</g>
</svg>
</button>
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn">Войти</a>
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
<button class="btn" type="submit">Выйти</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
<div class="page d-flex flex-column min-vh-100">
<nav class="navbar b3-navbar navbar-expand-md sticky-top">
<div class="container">
<a th:href="@{/}" class="navbar-brand">LMS<b>.</b></a>
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileNav"
aria-controls="mobileNav" aria-label="Меню">
<i class="bi bi-list"></i>
</button>
<div class="offcanvas offcanvas-end app-offcanvas" tabindex="-1" id="mobileNav" aria-labelledby="mobileNavLabel">
<div class="offcanvas-header">
<a th:href="@{/learn}" class="navbar-brand" id="mobileNavLabel">LMS<b>.</b></a>
<button type="button" class="navbar-close" data-bs-dismiss="offcanvas" aria-label="Закрыть">✕</button>
</div>
<div class="offcanvas-body">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/groups}">Группы</a></li>
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/users}">Пользователи</a></li>
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/labs}">Лабораторные</a></li>
<li class="nav-item" th:if="${currentUser != null and !currentUser.admin}"><a class="nav-link" th:href="@{/student/labs}">Лабораторные</a></li>
<li class="nav-item" th:if="${currentUser != null}"><a class="nav-link" th:href="@{/change-password}">Смена пароля</a></li>
</ul>
<div class="navbar-user d-flex flex-column flex-md-row align-items-start align-items-md-center gap-2">
<a th:href="@{/admin}" th:if="${currentUser != null and currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
<a th:href="@{/student}" th:if="${currentUser != null and !currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-secondary btn-sm theme-btn" aria-label="Сменить тему" title="Сменить тему">
<i class="bi bi-circle-half"></i>
</button>
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn btn-sm">Войти</a>
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post" class="m-0">
<button class="btn btn-sm" type="submit">Выйти</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="page-main">
</nav>
<main class="container page-main flex-grow-1">
<th:block th:replace="${content}" />
</main>
<footer class="page-footer">
<footer class="page-footer mt-auto">
LMS v1.1.0
</footer>
</div>
@@ -95,12 +94,12 @@
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
</div>
</div>
<dialog id="modal-confirm" class="modal">
<div class="modal-header">
<h2 class="modal-title" id="confirm-title">Подтверждение</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-confirm" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title" id="confirm-title">Подтверждение</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<p class="confirm-text" id="confirm-text">—</p>
<div class="flex justify-end gap-2 mt-4">
<button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button>

View File

@@ -5,52 +5,52 @@
<div th:fragment="toolbar(target)" class="md-toolbar" th:attr="data-md-target=${target}"
role="toolbar" aria-label="Форматирование текста">
<button type="button" class="md-btn" data-md="bold" title="Полужирный" aria-label="Полужирный">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M7 5h5.2a3.4 3.4 0 0 1 0 6.8H7z"/><path d="M7 11.8h6a3.6 3.6 0 0 1 0 7.2H7z"/></svg>
<i class="md-ico ico bi bi-type-bold" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="italic" title="Курсив" aria-label="Курсив">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M11 5h6M7 19h6M15 5l-4 14"/></svg>
<i class="md-ico ico bi bi-type-italic" aria-hidden="true"></i>
</button>
<span class="md-sep"></span>
<button type="button" class="md-btn" data-md="h2" title="Заголовок раздела" aria-label="Заголовок раздела">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M4 6v12M11 6v12M4 12h7"/><text x="14.5" y="18.2" font-size="10" font-weight="700" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">2</text></svg>
<i class="md-ico ico bi bi-type-h2" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="ul" title="Маркированный список" aria-label="Маркированный список">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.6" cy="6" r="1.4" fill="currentColor" stroke="none"/><circle cx="4.6" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="4.6" cy="18" r="1.4" fill="currentColor" stroke="none"/></svg>
<i class="md-ico ico bi bi-list-ul" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="ol" title="Нумерованный список" aria-label="Нумерованный список">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M11 6h9M11 12h9M11 18h9"/><text x="3" y="8.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">1</text><text x="3" y="14.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">2</text><text x="3" y="20.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">3</text></svg>
<i class="md-ico ico bi bi-list-ol" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="quote" title="Цитата" aria-label="Цитата">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M5 6v12" stroke-width="3"/><path d="M10 8h9M10 12h9M10 16h6"/></svg>
<i class="md-ico ico bi bi-quote" aria-hidden="true"></i>
</button>
<span class="md-sep"></span>
<button type="button" class="md-btn" data-md="link" title="Ссылка" aria-label="Ссылка">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M10.5 13.5a4 4 0 0 1 0-5.66l2.12-2.12a4 4 0 0 1 5.66 5.66l-1.2 1.2"/><path d="M13.5 10.5a4 4 0 0 1 0 5.66l-2.12 2.12a4 4 0 0 1-5.66-5.66l1.2-1.2"/></svg>
<i class="md-ico ico bi bi-link-45deg" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="code" title="Код в строке" aria-label="Код в строке">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M9 8l-4 4 4 4M15 8l4 4-4 4"/></svg>
<i class="md-ico ico bi bi-code" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="codeblock" title="Блок кода" aria-label="Блок кода">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M9.5 10l-2 2 2 2M14.5 10l2 2-2 2"/></svg>
<i class="md-ico ico bi bi-code-square" aria-hidden="true"></i>
</button>
<span class="md-sep"></span>
<button type="button" class="md-btn" data-md="table" title="Таблица" aria-label="Таблица">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9.7h18M9 9.7V19M15 9.7V19M3 14.3h18"/></svg>
<i class="md-ico ico bi bi-table" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="figure" title="Картинка с подписью" aria-label="Картинка с подписью">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.5"/><path d="M4 17l4.5-4.5 3 3 3-3L20 17"/></svg>
<i class="md-ico ico bi bi-image" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="video" title="Видео" aria-label="Видео">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M10 9.5l5 2.5-5 2.5z" fill="currentColor" stroke="none"/></svg>
<i class="md-ico ico bi bi-camera-video" aria-hidden="true"></i>
</button>
<button type="button" class="md-btn" data-md="spoiler" title="Спойлер (скрытый текст)" aria-label="Спойлер">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M3 3l18 18"/><path d="M10.6 6.2A9.7 9.7 0 0 1 12 6c5 0 9 6 9 6a15.4 15.4 0 0 1-3.5 4M6.4 8.1A15.3 15.3 0 0 0 3 12s4 6 9 6a9.6 9.6 0 0 0 3.1-.5"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
<i class="md-ico ico bi bi-eye-slash" aria-hidden="true"></i>
</button>
</div>
</body>

View File

@@ -46,21 +46,21 @@
</h1>
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
<button class="btn btn-primary" type="button" data-action="article-edit">Править</button>
<button class="mini-btn" type="button" data-action="article-edit" title="Править статью" aria-label="Править статью"><i class="ico bi bi-pencil" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
<input type="hidden" name="published" th:value="${!article.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<a class="btn btn-secondary"
th:href="@{/learn/{c}(c=${article.chapterSlug})}">К главе</a>
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${backParent}">
<button class="mini-btn danger" type="submit" title="Удалить статью" aria-label="Удалить статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить статью" aria-label="Удалить статью"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
@@ -124,7 +124,7 @@
</div>
<script th:src="@{/js/mdeditor.js(v='11')}" defer></script>
<script th:src="@{/js/mdeditor.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -16,19 +16,19 @@
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
<button class="btn btn-primary" type="button" data-modal="modal-new-section">+ Раздел</button>
<button class="mini-btn" type="button" data-modal="modal-rename-chapter" title="Переименовать главу" aria-label="Переименовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
<button class="mini-btn" type="button" data-modal="modal-rename-chapter" title="Переименовать главу" aria-label="Переименовать главу"><i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/chapters/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${chapter.id}">
<input type="hidden" name="published" th:value="${!chapter.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${chapter.published}" title="Скрыть главу" aria-label="Скрыть главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${chapter.published}" title="Опубликовать главу" aria-label="Опубликовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${chapter.published}" title="Скрыть главу" aria-label="Скрыть главу"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${chapter.published}" title="Опубликовать главу" aria-label="Опубликовать главу"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post" data-confirm="Удалить главу со всеми разделами и статьями?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${backParent}">
<button class="mini-btn danger" type="submit" title="Удалить главу со всеми разделами и статьями" aria-label="Удалить главу со всеми разделами и статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить главу со всеми разделами и статьями" aria-label="Удалить главу со всеми разделами и статьями"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
@@ -50,30 +50,30 @@
<div class="sub-head sort-row">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
title="Перетащите мышью: изменить порядок разделов">⠿</span>
title="Перетащите мышью: изменить порядок разделов"><i class="bi bi-grip-vertical"></i></span>
<button type="button" class="sub-toggle" aria-expanded="false"
title="Свернуть или развернуть раздел">
<span class="sub-arrow" aria-hidden="true"></span>
<span class="sub-arrow" aria-hidden="true"><i class="cell-ico ico bi bi-chevron-down" aria-hidden="true"></i></span>
</button>
<a class="group-title sort-title m-0"
th:href="@{/learn/{c}/{s}(c=${chapter.slug}, s=${section.slug})}"
th:text="${section.title}">Раздел</a>
<span th:if="${!section.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${section.id}">
<input type="hidden" name="published" th:value="${!section.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><i class="cell-ico ico bi bi-arrow-left-right" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел вместе со статьями?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn danger" type="submit" title="Удалить раздел вместе со статьями" aria-label="Удалить раздел вместе со статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить раздел вместе со статьями" aria-label="Удалить раздел вместе со статьями"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
</div>
@@ -83,21 +83,21 @@
<li th:each="article : ${section.articles}" th:attr="data-article-id=${article.id}">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='article',data-drag-id=${article.id}"
title="Перетащите мышью: изменить порядок или перенести в другой раздел">⠿</span>
title="Перетащите мышью: изменить порядок или перенести в другой раздел"><i class="bi bi-grip-vertical"></i></span>
<a class="sort-title"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug})}"
th:text="${article.title}">Статья</a>
<span th:if="${!article.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
<a class="btn btn-secondary"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
<a class="mini-btn" title="Править статью" aria-label="Править статью"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}"><i class="ico bi bi-pencil" aria-hidden="true"></i></a>
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
<input type="hidden" name="published" th:value="${!article.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
</div>
</li>
@@ -106,13 +106,13 @@
</div>
<th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-section-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
<dialog th:id="|modal-rename-section-${section.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать раздел</h2>
<button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
@@ -122,13 +122,13 @@
</form>
</div>
</dialog>
<dialog th:id="|modal-move-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Перенести статьи из раздела</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
<dialog th:id="|modal-move-${section.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Перенести статьи из раздела</h2>
<button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-move-${section.id}|">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
@@ -150,12 +150,12 @@
</div>
</div>
<dialog id="modal-rename-chapter" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать главу</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-rename-chapter" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать главу</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
@@ -166,12 +166,12 @@
</div>
</dialog>
<dialog id="modal-new-section" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новый раздел в главе</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-new-section" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новый раздел в главе</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="chapterId" th:value="${chapter.id}">
@@ -183,7 +183,7 @@
</div>
</dialog>
<script th:src="@{/js/content-sort.js(v='11')}" defer></script>
<script th:src="@{/js/content-sort.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -3,12 +3,16 @@
<head><title>Материалы — LMS</title></head>
<body>
<div id="content" th:attr="data-csrf=${csrfToken},data-sort-scope='full'">
<div class="flex items-center justify-between gap-4 mb-6">
<h1 class="page-title">Учебные материалы</h1>
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
<button class="btn btn-primary" type="button" data-modal="modal-new-article">+ Статья</button>
<button class="btn btn-secondary" type="button" data-modal="modal-import">Импорт zip</button>
<button class="btn btn-secondary" type="button" data-modal="modal-new-chapter">+ Глава</button>
<div class="row g-3 align-items-center mb-4">
<div class="col-12 col-md">
<h1 class="page-title m-0">Учебные материалы</h1>
</div>
<div class="col-12 col-md-auto" th:if="${currentUser != null and currentUser.admin}">
<div class="row row-cols-3 row-cols-md-auto g-2">
<div class="col"><button class="btn btn-secondary w-100" type="button" data-modal="modal-import">Импорт</button></div>
<div class="col"><button class="btn primary w-100" type="button" data-modal="modal-new-chapter">+ Глава</button></div>
<div class="col"><button class="btn btn-primary w-100" type="button" data-modal="modal-new-article">+ Статья</button></div>
</div>
</div>
</div>
<div th:if="${currentUser != null and currentUser.admin}" id="sort-status" class="sort-status" hidden>
@@ -24,7 +28,7 @@
<div class="sort-row">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='chapter',data-drag-id=${chapter.id}"
title="Перетащите мышью: изменить порядок глав">⠿</span>
title="Перетащите мышью: изменить порядок глав"><i class="bi bi-grip-vertical"></i></span>
<h2 class="section-title sort-title m-0" th:text="${chapter.title}">Глава</h2>
<span th:if="${!chapter.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
@@ -32,13 +36,13 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${chapter.id}">
<input type="hidden" name="published" th:value="${!chapter.published}">
<button class="mini-btn" type="submit" th:if="${chapter.published}" title="Скрыть главу" aria-label="Скрыть главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${chapter.published}" title="Опубликовать главу" aria-label="Опубликовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${chapter.published}" title="Скрыть главу" aria-label="Скрыть главу"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${chapter.published}" title="Опубликовать главу" aria-label="Опубликовать главу"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-chapter-${chapter.id}|" title="Переименовать главу" aria-label="Переименовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-chapter-${chapter.id}|" title="Переименовать главу" aria-label="Переименовать главу"><i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post" data-confirm="Удалить главу со всеми разделами и статьями?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="mini-btn danger" type="submit" title="Удалить главу со всеми разделами и статьями" aria-label="Удалить главу со всеми разделами и статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить главу со всеми разделами и статьями" aria-label="Удалить главу со всеми разделами и статьями"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
</div>
@@ -51,10 +55,10 @@
<div class="sub-head sort-row">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
title="Перетащите мышью: изменить порядок или перенести раздел">⠿</span>
title="Перетащите мышью: изменить порядок или перенести раздел"><i class="bi bi-grip-vertical"></i></span>
<button type="button" class="sub-toggle" aria-expanded="false"
title="Свернуть или развернуть раздел">
<span class="sub-arrow" aria-hidden="true"></span>
<span class="sub-arrow" aria-hidden="true"><i class="cell-ico ico bi bi-chevron-down" aria-hidden="true"></i></span>
</button>
<a class="group-title sort-title m-0"
th:href="@{/learn/{c}/{s}(c=${chapter.slug}, s=${section.slug})}"
@@ -65,14 +69,14 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${section.id}">
<input type="hidden" name="published" th:value="${!section.published}">
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи в другой раздел" aria-label="Перенести статьи в другой раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i></button>
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи в другой раздел" aria-label="Перенести статьи в другой раздел"><i class="cell-ico ico bi bi-arrow-left-right" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел со всеми статьями?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="mini-btn danger" type="submit" title="Удалить раздел со всеми статьями" aria-label="Удалить раздел со всеми статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить раздел со всеми статьями" aria-label="Удалить раздел со всеми статьями"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
</div>
@@ -82,7 +86,7 @@
<li th:each="article : ${section.articles}" th:attr="data-article-id=${article.id}">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='article',data-drag-id=${article.id}"
title="Перетащите мышью: изменить порядок или перенести статью">⠿</span>
title="Перетащите мышью: изменить порядок или перенести статью"><i class="bi bi-grip-vertical"></i></span>
<a class="sort-title"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug})}"
th:text="${article.title}">Статья</a>
@@ -92,14 +96,14 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
<input type="hidden" name="published" th:value="${!article.published}">
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<a class="btn btn-secondary"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
<a class="mini-btn" title="Править статью" aria-label="Править статью"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}"><i class="ico bi bi-pencil" aria-hidden="true"></i></a>
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="mini-btn danger" type="submit" title="Удалить статью" aria-label="Удалить статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить статью" aria-label="Удалить статью"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
</li>
@@ -108,13 +112,13 @@
</div>
<th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-section-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
<dialog th:id="|modal-rename-section-${section.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать раздел</h2>
<button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label>
@@ -123,13 +127,13 @@
</form>
</div>
</dialog>
<dialog th:id="|modal-move-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Перенести статьи из раздела</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
<dialog th:id="|modal-move-${section.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Перенести статьи из раздела</h2>
<button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-move-${section.id}|">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted">Выберите раздел-приёмник: статьи перенесутся туда, а текущий раздел будет удалён.</p>
@@ -148,13 +152,13 @@
</div>
<th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать главу</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать главу</h2>
<button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-chapter-${chapter.id}|">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label>
@@ -166,12 +170,12 @@
</th:block>
</div>
<dialog id="modal-import" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт материалов (zip)</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-import" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Импорт материалов (zip)</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted">
@@ -192,12 +196,12 @@
</div>
</dialog>
<dialog id="modal-new-chapter" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая глава</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-new-chapter" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новая глава</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="chTitle">Название</label>
@@ -207,12 +211,12 @@
</div>
</dialog>
<dialog id="modal-new-article" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая статья</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-new-article" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новая статья</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="naChapter">Существующая глава и раздел</label>
@@ -232,8 +236,8 @@
</div>
</dialog>
<script th:src="@{/js/mdeditor.js(v='11')}" defer></script>
<script th:src="@{/js/content-sort.js(v='11')}" defer></script>
<script th:src="@{/js/mdeditor.js(v='19')}" defer></script>
<script th:src="@{/js/content-sort.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -18,20 +18,20 @@
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
<button class="btn btn-primary" type="button" data-modal="modal-new-article">+ Статья</button>
<button class="mini-btn" type="button" data-modal="modal-rename-section" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
<button class="mini-btn" type="button" data-modal="modal-rename-section" title="Переименовать раздел" aria-label="Переименовать раздел"><i class="cell-ico ico bi bi-pencil" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${section.id}">
<input type="hidden" name="published" th:value="${!section.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<button class="mini-btn" type="button" data-modal="modal-move-section" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
<button class="mini-btn" type="button" data-modal="modal-move-section" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><i class="cell-ico ico bi bi-arrow-left-right" aria-hidden="true"></i></button>
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел вместе со статьями?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${backParent}">
<button class="mini-btn danger" type="submit" title="Удалить раздел вместе со статьями" aria-label="Удалить раздел вместе со статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
<button class="mini-btn danger" type="submit" title="Удалить раздел вместе со статьями" aria-label="Удалить раздел вместе со статьями"><i class="cell-ico ico bi bi-trash" aria-hidden="true"></i></button>
</form>
</div>
@@ -50,21 +50,21 @@
<td>
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='article',data-drag-id=${article.id}"
title="Перетащите, чтобы изменить порядок статей">⠿</span>
title="Перетащите, чтобы изменить порядок статей"><i class="bi bi-grip-vertical"></i></span>
<a th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug})}"
th:text="${article.title}">—</a>
<span th:if="${!article.published}" class="badge badge-warn ml-1">скрыто</span>
</td>
<td class="text-right actions-cell">
<a class="btn btn-secondary"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
<a class="mini-btn" title="Править статью" aria-label="Править статью"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}"><i class="ico bi bi-pencil" aria-hidden="true"></i></a>
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
<input type="hidden" name="published" th:value="${!article.published}">
<input type="hidden" name="back" th:value="${back}">
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
<button class="mini-btn" type="submit" th:if="${article.published}" title="Скрыть статью" aria-label="Скрыть статью"><i class="cell-ico ico bi bi-eye" aria-hidden="true"></i></button>
<button class="mini-btn" type="submit" th:unless="${article.published}" title="Опубликовать статью" aria-label="Опубликовать статью"><i class="cell-ico ico bi bi-eye-slash" aria-hidden="true"></i></button>
</form>
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
@@ -78,12 +78,12 @@
</table>
</div>
<dialog id="modal-rename-section" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-rename-section" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Переименовать раздел</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
@@ -94,12 +94,12 @@
</div>
</dialog>
<dialog id="modal-move-section" class="modal">
<div class="modal-header">
<h2 class="modal-title">Перенести статьи и удалить раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-move-section" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Перенести статьи и удалить раздел</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${backParent}">
@@ -118,12 +118,12 @@
</div>
</dialog>
<dialog id="modal-new-article" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая статья в разделе</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
<dialog id="modal-new-article" class="dlg">
<div class="dlg-header">
<h2 class="dlg-title">Новая статья в разделе</h2>
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="modal-body">
<div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="sectionId" th:value="${section.id}">
@@ -143,7 +143,7 @@
</div>
</dialog>
<script th:src="@{/js/content-sort.js(v='11')}" defer></script>
<script th:src="@{/js/content-sort.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -5,19 +5,19 @@
<div id="content">
<h1 class="page-title">Здравствуйте, <span th:text="${currentUser.name}">студент</span></h1>
<div class="grid grid-stats">
<div class="card card-body"><p class="stat-title">Сдано работ</p>
<p class="stat-value" th:text="${submittedCount}">0</p></div>
<div class="card card-body"><p class="stat-title">Средний балл</p>
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-3">
<div class="col"><div class="card card-body h-100"><p class="stat-title">Сдано работ</p>
<p class="stat-value" th:text="${submittedCount}">0</p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Средний балл</p>
<p class="stat-value" th:if="${average != null}" th:text="${average}">—</p>
<p class="stat-value text-muted" th:if="${average == null}">—</p></div>
<div class="card card-body"><p class="stat-title">Лучшая оценка</p>
<p class="stat-value text-muted" th:if="${average == null}">—</p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Лучшая оценка</p>
<p class="stat-value" th:if="${best != null}" th:text="${best.toPlainString()}">—</p>
<p class="stat-value text-muted" th:if="${best == null}">—</p></div>
<div class="card card-body"><p class="stat-title">Слабое место</p>
<p class="stat-value text-muted" th:if="${best == null}">—</p></div></div>
<div class="col"><div class="card card-body h-100"><p class="stat-title">Слабое место</p>
<p class="stat-value" th:if="${worst != null}" th:text="${worst.toPlainString()}">—</p>
<p class="stat-value text-muted" th:if="${worst == null}">—</p></div>
</div>
<p class="stat-value text-muted" th:if="${worst == null}">—</p></div></div>
</div>
<div class="flex flex-wrap gap-2 mt-8">
<a class="btn btn-primary" th:href="@{/student/labs}">Лабораторные</a>
@@ -25,8 +25,8 @@
</div>
<h2 class="section-title mt-8">Мои лабораторные</h2>
<div class="grid grid-2">
<div th:each="c : ${cards}" class="card">
<div class="row row-cols-1 row-cols-lg-2 g-4">
<div th:each="c : ${cards}" class="col"><div class="card h-100">
<div class="card-body">
<h3 class="section-title">
<a th:href="@{/student/labs/{id}(id=${c.id})}" th:text="|Лабораторная работа ${c.number}|">ЛР</a>
@@ -40,6 +40,7 @@
</div>
</div>
</div>
</div>
<div th:if="${#lists.isEmpty(cards)}" class="card card-body text-muted">Лабораторных пока нет.</div>
</div>
</div>

View File

@@ -107,7 +107,7 @@
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
</div>
</div>
<script th:src="@{/js/defense-timer.js(v='11')}" defer></script>
<script th:src="@{/js/defense-timer.js(v='19')}" defer></script>
</div>
</body>
</html>

View File

@@ -4,8 +4,8 @@
<body>
<div id="content">
<h1 class="page-title">Лабораторные работы</h1>
<div class="grid grid-2">
<div th:each="c : ${cards}" class="card">
<div class="row row-cols-1 row-cols-lg-2 g-4">
<div th:each="c : ${cards}" class="col"><div class="card h-100">
<div class="card-body">
<h2 class="section-title">
<a th:href="@{/student/labs/{id}(id=${c.id})}" th:text="|Лабораторная работа ${c.number}. ${c.title}|">ЛР</a>
@@ -23,6 +23,7 @@
</p>
</div>
</div>
</div>
<div th:if="${#lists.isEmpty(cards)}" class="card card-body text-muted">Опубликованных лабораторных пока нет.</div>
</div>
</div>