Compare commits
7 Commits
743dd93bc2
...
c7b7de0dfc
| Author | SHA1 | Date | |
|---|---|---|---|
| c7b7de0dfc | |||
| f5ad352e20 | |||
| 4063cc0ca8 | |||
| 9a4e11e5ba | |||
| 24b72a2525 | |||
| 1c5c5651c7 | |||
| 476cb4d91a |
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
5
web/src/main/resources/static/css/bootstrap-icons.min.css
vendored
Normal file
5
web/src/main/resources/static/css/bootstrap-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
web/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
6
web/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
web/src/main/resources/static/css/fonts/bootstrap-icons.woff
Normal file
BIN
web/src/main/resources/static/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
web/src/main/resources/static/css/fonts/bootstrap-icons.woff2
Normal file
BIN
web/src/main/resources/static/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
7
web/src/main/resources/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
web/src/main/resources/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user