No inline JS/styles in templates: app.js delegation (data-action/data-modal/data-confirm/data-auto-submit), B3 confirm/alert dialogs, theme-init file
This commit is contained in:
@@ -287,6 +287,8 @@ tr:last-child td { border-bottom: none; }
|
|||||||
to { opacity: 1; transform: none; }
|
to { opacity: 1; transform: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-text { margin: 0; font-size: 0.875rem; white-space: pre-line; overflow-wrap: anywhere; }
|
||||||
|
|
||||||
/* Modals */
|
/* Modals */
|
||||||
dialog.modal {
|
dialog.modal {
|
||||||
border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: 0;
|
border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: 0;
|
||||||
|
|||||||
@@ -520,7 +520,10 @@ function openVariant(btn) {
|
|||||||
fetch('/admin/labs/' + encodeURIComponent(lab) + '/schema')
|
fetch('/admin/labs/' + encodeURIComponent(lab) + '/schema')
|
||||||
.then(function (r) { return r.ok && !r.redirected ? r.json() : { levels: [] }; })
|
.then(function (r) { return r.ok && !r.redirected ? r.json() : { levels: [] }; })
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
alert('Не удалось загрузить уровни и варианты (возможно, сессия истекла). Обновите страницу и откройте модалку заново.');
|
window.showAlert({
|
||||||
|
title: 'Не удалось открыть',
|
||||||
|
text: 'Не удалось загрузить уровни и варианты (возможно, сессия истекла). Обновите страницу и откройте модалку заново.'
|
||||||
|
});
|
||||||
return { levels: [] };
|
return { levels: [] };
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
@@ -637,14 +640,21 @@ window.openLabAssign = function (btn) {
|
|||||||
window.confirmAssignVariant = function (sel) {
|
window.confirmAssignVariant = function (sel) {
|
||||||
var opt = sel.selectedOptions[0];
|
var opt = sel.selectedOptions[0];
|
||||||
var taken = opt ? parseInt(opt.getAttribute('data-taken') || '0', 10) : 0;
|
var taken = opt ? parseInt(opt.getAttribute('data-taken') || '0', 10) : 0;
|
||||||
|
var submit = function () {
|
||||||
|
sel.dataset.prev = sel.value;
|
||||||
|
sel.closest('form').requestSubmit();
|
||||||
|
};
|
||||||
if (opt && opt.value && taken > 0 && sel.value !== sel.dataset.prev) {
|
if (opt && opt.value && taken > 0 && sel.value !== sel.dataset.prev) {
|
||||||
if (!confirm('Вариант уже занят (' + taken + '). Назначить всё равно?')) {
|
window.showConfirm({
|
||||||
sel.value = sel.dataset.prev;
|
text: 'Вариант уже занят (' + taken + '). Назначить всё равно?',
|
||||||
return;
|
okText: 'Назначить'
|
||||||
}
|
}).then(function (ok) {
|
||||||
|
if (ok) submit();
|
||||||
|
else sel.value = sel.dataset.prev;
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
sel.dataset.prev = sel.value;
|
submit();
|
||||||
sel.closest('form').requestSubmit();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Формы внутри модалки сохраняются через fetch — модалка не закрывается и обновляется */
|
/* Формы внутри модалки сохраняются через fetch — модалка не закрывается и обновляется */
|
||||||
|
|||||||
247
web/src/main/resources/static/js/app.js
Normal file
247
web/src/main/resources/static/js/app.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/* Общая логика интерфейса: тема, подсветка кода, модалки, подтверждения,
|
||||||
|
уведомления, скролл-лок при открытых диалогах, активный пункт меню.
|
||||||
|
Никаких инлайновых обработчиков в шаблонах — только классы и data-атрибуты. */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Тема */
|
||||||
|
window.toggleTheme = function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
var t = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||||
|
root.setAttribute('data-theme', t);
|
||||||
|
try { localStorage.setItem('lms-theme', t); } catch (e) { /* приватный режим */ }
|
||||||
|
window.__hljsSet(t);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Подсветка кода */
|
||||||
|
window.__hljsSet = function (theme) {
|
||||||
|
var l = document.getElementById('hljs-light'), d = document.getElementById('hljs-dark');
|
||||||
|
if (l && d) { l.disabled = theme !== 'light'; d.disabled = theme !== 'dark'; }
|
||||||
|
};
|
||||||
|
window.__hljsAll = function () {
|
||||||
|
if (!window.hljs) return;
|
||||||
|
document.querySelectorAll('pre code').forEach(function (el) { window.hljs.highlightElement(el); });
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Модалки */
|
||||||
|
window.openModal = function (id) {
|
||||||
|
var d = document.getElementById(id);
|
||||||
|
if (d) d.showModal();
|
||||||
|
syncModalScrollLock();
|
||||||
|
};
|
||||||
|
window.closeModal = function (id) {
|
||||||
|
var d = document.getElementById(id);
|
||||||
|
if (d) d.close();
|
||||||
|
syncModalScrollLock();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Скролл-лок: пока открыт диалог, страница остаётся на месте */
|
||||||
|
var modalScrollY = 0;
|
||||||
|
function syncModalScrollLock() {
|
||||||
|
var root = document.documentElement;
|
||||||
|
if (document.querySelector('dialog[open]')) {
|
||||||
|
if (root.classList.contains('modal-open')) return;
|
||||||
|
modalScrollY = window.scrollY || window.pageYOffset || 0;
|
||||||
|
var sw = window.innerWidth - root.clientWidth;
|
||||||
|
root.style.setProperty('--modal-scrollbar', (sw > 0 ? sw : 0) + 'px');
|
||||||
|
root.style.setProperty('--modal-top', (-modalScrollY) + 'px');
|
||||||
|
root.classList.add('modal-open');
|
||||||
|
} else if (root.classList.contains('modal-open')) {
|
||||||
|
root.classList.remove('modal-open');
|
||||||
|
root.style.removeProperty('--modal-scrollbar');
|
||||||
|
root.style.removeProperty('--modal-top');
|
||||||
|
window.scrollTo(0, modalScrollY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('toggle', function (e) {
|
||||||
|
if (e.target && e.target.tagName === 'DIALOG') syncModalScrollLock();
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
/* Секции материалов: раскрытие и запоминание состояния */
|
||||||
|
function sectionStateKey(id) { return 'lms-section-open:' + id; }
|
||||||
|
function applySectionState(acc) {
|
||||||
|
var id = acc.getAttribute('data-section-id');
|
||||||
|
var open = sessionStorage.getItem(sectionStateKey(id)) === '1';
|
||||||
|
var def = acc.getAttribute('data-default-open') === 'true';
|
||||||
|
acc.classList.toggle('collapsed', !open && !def);
|
||||||
|
var btn = acc.querySelector('.sub-toggle');
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', String(!acc.classList.contains('collapsed')));
|
||||||
|
}
|
||||||
|
function initSections() {
|
||||||
|
document.querySelectorAll('.sub-acc[data-section-id]').forEach(applySectionState);
|
||||||
|
}
|
||||||
|
window.toggleSection = function (btn) {
|
||||||
|
var acc = btn.closest('.sub-acc');
|
||||||
|
if (!acc) return;
|
||||||
|
acc.classList.toggle('collapsed');
|
||||||
|
var collapsed = acc.classList.contains('collapsed');
|
||||||
|
btn.setAttribute('aria-expanded', String(!collapsed));
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(sectionStateKey(acc.getAttribute('data-section-id')), collapsed ? '0' : '1');
|
||||||
|
} catch (e) { /* приватный режим */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Активный пункт меню */
|
||||||
|
function initNavActive() {
|
||||||
|
var path = window.location.pathname;
|
||||||
|
document.querySelectorAll('#top-nav .nav-link').forEach(function (a) {
|
||||||
|
var href = a.getAttribute('href') || '';
|
||||||
|
if (path === href || (href !== '/' && path.indexOf(href + '/') === 0)) a.classList.add('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Серверные уведомления: автоскрытие и кнопка закрытия */
|
||||||
|
function initDataToasts() {
|
||||||
|
document.querySelectorAll('[data-toast]').forEach(function (toast) {
|
||||||
|
var timer = setTimeout(function () { toast.remove(); }, 30000);
|
||||||
|
var close = toast.querySelector('.toast-close');
|
||||||
|
if (close) close.addEventListener('click', function () { clearTimeout(timer); toast.remove(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
window.__hljsSet(document.documentElement.getAttribute('data-theme'));
|
||||||
|
window.__hljsAll();
|
||||||
|
initNavActive();
|
||||||
|
initSections();
|
||||||
|
initDataToasts();
|
||||||
|
syncModalScrollLock();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- Делегирование: вместо onclick/onsubmit/onchange в разметке ---- */
|
||||||
|
|
||||||
|
/* Диалог подтверждения/уведомления — замена нативным alert и confirm */
|
||||||
|
var confirmResolve = null;
|
||||||
|
function confirmFinish(result) {
|
||||||
|
var r = confirmResolve;
|
||||||
|
confirmResolve = null;
|
||||||
|
if (r) r(result);
|
||||||
|
}
|
||||||
|
(function () {
|
||||||
|
var d = document.getElementById('modal-confirm');
|
||||||
|
if (!d) return;
|
||||||
|
var ok = d.querySelector('#confirm-ok');
|
||||||
|
var cancel = d.querySelector('#confirm-cancel');
|
||||||
|
ok.addEventListener('click', function () { confirmFinish(true); d.close(); });
|
||||||
|
cancel.addEventListener('click', function () { confirmFinish(false); d.close(); });
|
||||||
|
d.addEventListener('close', function () { confirmFinish(false); });
|
||||||
|
})();
|
||||||
|
|
||||||
|
window.showConfirm = function (opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var d = document.getElementById('modal-confirm');
|
||||||
|
if (!d) return Promise.resolve(window.confirm(opts.text || ''));
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
document.getElementById('confirm-title').textContent = opts.title || 'Подтверждение';
|
||||||
|
document.getElementById('confirm-text').textContent = opts.text || '';
|
||||||
|
var ok = document.getElementById('confirm-ok');
|
||||||
|
var cancel = document.getElementById('confirm-cancel');
|
||||||
|
ok.textContent = opts.okText || 'ОК';
|
||||||
|
ok.classList.toggle('btn-danger', !!opts.danger);
|
||||||
|
ok.classList.toggle('btn-primary', !opts.danger);
|
||||||
|
cancel.textContent = opts.cancelText || 'Отмена';
|
||||||
|
cancel.hidden = !!opts.hideCancel;
|
||||||
|
confirmResolve = resolve;
|
||||||
|
window.openModal('modal-confirm');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.showAlert = function (opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
return window.showConfirm({
|
||||||
|
title: opts.title || 'Внимание',
|
||||||
|
text: opts.text || '',
|
||||||
|
okText: 'Понятно',
|
||||||
|
hideCancel: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Подтверждения: data-confirm на кнопке — спрашиваем при клике,
|
||||||
|
на форме — при сабмите (Enter или кнопка без data-confirm). */
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var el = e.target.closest('[data-confirm]');
|
||||||
|
if (!el || el.tagName === 'FORM') return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
var danger = el.classList.contains('btn-danger') || el.classList.contains('danger');
|
||||||
|
window.showConfirm({
|
||||||
|
text: el.getAttribute('data-confirm'),
|
||||||
|
danger: danger,
|
||||||
|
okText: danger ? 'Удалить' : 'Продолжить'
|
||||||
|
}).then(function (ok) {
|
||||||
|
if (!ok) return;
|
||||||
|
if (el.tagName === 'BUTTON' && el.form) el.form.requestSubmit(el);
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
var f = e.target;
|
||||||
|
if (!f.matches('form[data-confirm]') || f.dataset.confirmed) return;
|
||||||
|
e.preventDefault();
|
||||||
|
var danger = f.classList.contains('btn-danger');
|
||||||
|
window.showConfirm({
|
||||||
|
text: f.getAttribute('data-confirm'),
|
||||||
|
danger: danger,
|
||||||
|
okText: danger ? 'Удалить' : 'Продолжить'
|
||||||
|
}).then(function (ok) {
|
||||||
|
if (!ok) return;
|
||||||
|
f.dataset.confirmed = '1';
|
||||||
|
f.requestSubmit();
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
/* Клик-действия */
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var el = e.target;
|
||||||
|
|
||||||
|
var close = el.closest('.modal-close');
|
||||||
|
if (close) {
|
||||||
|
var dlg = close.closest('dialog');
|
||||||
|
if (dlg) dlg.close();
|
||||||
|
syncModalScrollLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toast = el.closest('.toast-close');
|
||||||
|
if (toast) {
|
||||||
|
var item = toast.closest('.toast-item');
|
||||||
|
if (item) item.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var themeBtn = el.closest('.theme-btn');
|
||||||
|
if (themeBtn) { window.toggleTheme(); return; }
|
||||||
|
|
||||||
|
var toggle = el.closest('.sub-toggle');
|
||||||
|
if (toggle) { window.toggleSection(toggle); return; }
|
||||||
|
|
||||||
|
var opener = el.closest('[data-modal]');
|
||||||
|
if (opener) { window.openModal(opener.getAttribute('data-modal')); return; }
|
||||||
|
|
||||||
|
var action = el.closest('[data-action]');
|
||||||
|
if (!action) return;
|
||||||
|
var kind = action.getAttribute('data-action');
|
||||||
|
var fn = {
|
||||||
|
grade: window.openGradeEditor,
|
||||||
|
variant: window.openVariant,
|
||||||
|
defense: window.openDefense,
|
||||||
|
assign: window.openLabAssign,
|
||||||
|
'article-edit': window.startArticleEdit,
|
||||||
|
'article-edit-cancel': window.cancelArticleEdit
|
||||||
|
}[kind];
|
||||||
|
if (typeof fn === 'function') fn(action);
|
||||||
|
else if (kind === 'comment-add' && typeof window.csRow === 'function') window.csRow('', 'neutral');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Изменение полей: автоотправка форм (data-auto-submit),
|
||||||
|
подтверждение занятия занятого варианта (select.js-variant-select) */
|
||||||
|
document.addEventListener('change', function (e) {
|
||||||
|
var t = e.target;
|
||||||
|
if (t.matches('.js-variant-select') && typeof window.confirmAssignVariant === 'function') {
|
||||||
|
window.confirmAssignVariant(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t.matches('[data-auto-submit]')) {
|
||||||
|
var f = t.closest('form');
|
||||||
|
if (f) f.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -103,13 +103,20 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.cancelArticleEdit = function () {
|
window.cancelArticleEdit = function () {
|
||||||
if (dirty && !confirm('Выйти из редактирования без сохранения?')) return;
|
var proceed = function () {
|
||||||
var view = document.getElementById('article-view');
|
var view = document.getElementById('article-view');
|
||||||
var form = document.getElementById('article-edit');
|
var form = document.getElementById('article-edit');
|
||||||
if (!view || !form) return;
|
if (!view || !form) return;
|
||||||
form.hidden = true;
|
form.hidden = true;
|
||||||
view.hidden = false;
|
view.hidden = false;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
|
};
|
||||||
|
if (!dirty) { proceed(); return; }
|
||||||
|
window.showConfirm({
|
||||||
|
title: 'Несохранённые изменения',
|
||||||
|
text: 'Выйти из редактирования без сохранения?',
|
||||||
|
okText: 'Выйти'
|
||||||
|
}).then(function (ok) { if (ok) proceed(); });
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('input', function (event) {
|
document.addEventListener('input', function (event) {
|
||||||
|
|||||||
6
web/src/main/resources/static/js/theme-init.js
Normal file
6
web/src/main/resources/static/js/theme-init.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* Тема применяется до отрисовки, чтобы не было вспышки светлой темы. */
|
||||||
|
(function () {
|
||||||
|
var t;
|
||||||
|
try { t = localStorage.getItem('lms-theme') || 'light'; } catch (e) { t = 'light'; }
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
})();
|
||||||
@@ -33,8 +33,7 @@
|
|||||||
th:attr="data-refresh=${refreshUrl}">
|
th:attr="data-refresh=${refreshUrl}">
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="studentId" th:value="${row.studentId}">
|
<input type="hidden" name="studentId" th:value="${row.studentId}">
|
||||||
<select class="input" name="labLevelId" th:disabled="${#lists.isEmpty(lab.levels)}"
|
<select class="input" name="labLevelId" th:disabled="${#lists.isEmpty(lab.levels)}" data-auto-submit>
|
||||||
onchange="this.closest('form').requestSubmit()">
|
|
||||||
<option th:each="lv : ${lab.levels}" th:value="${lv.id}"
|
<option th:each="lv : ${lab.levels}" th:value="${lv.id}"
|
||||||
th:selected="${lv.id == row.levelId}" th:text="${lv.title}">Уровень</option>
|
th:selected="${lv.id == row.levelId}" th:text="${lv.title}">Уровень</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
th:attr="data-refresh=${refreshUrl}">
|
th:attr="data-refresh=${refreshUrl}">
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="studentId" th:value="${row.studentId}">
|
<input type="hidden" name="studentId" th:value="${row.studentId}">
|
||||||
<select class="input" name="variantId" onchange="confirmAssignVariant(this)">
|
<select class="input js-variant-select" name="variantId">
|
||||||
<option value="">— авто —</option>
|
<option value="">— авто —</option>
|
||||||
<optgroup th:each="lv : ${lab.levels}" th:label="${lv.title}">
|
<optgroup th:each="lv : ${lab.levels}" th:label="${lv.title}">
|
||||||
<option th:each="v : ${lv.variants}" th:value="${v.id}"
|
<option th:each="v : ${lv.variants}" th:value="${v.id}"
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/submissions.zip(id=${group.id})}"
|
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/submissions.zip(id=${group.id})}"
|
||||||
title="ZIP со всеми последними решениями группы по всем лабораторным">Скачать работы</a>
|
title="ZIP со всеми последними решениями группы по всем лабораторным">Скачать работы</a>
|
||||||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
|
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
|
||||||
<button class="btn" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
|
<button class="btn" type="button" data-modal="modal-add-student">+ Студент</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<input class="input table-search w-260" data-target="group-table"
|
<input class="input table-search w-260" data-target="group-table"
|
||||||
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
|
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
|
||||||
<select class="input" id="lab-focus" onchange="focusLab()">
|
<select class="input" id="lab-focus">
|
||||||
<option value="">все лабораторные</option>
|
<option value="">все лабораторные</option>
|
||||||
<option th:each="lab : ${labs}" th:value="${lab.id}" th:text="|ЛР ${lab.number}|">—</option>
|
<option th:each="lab : ${labs}" th:value="${lab.id}" th:text="|ЛР ${lab.number}|">—</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -47,8 +47,7 @@
|
|||||||
<span class="lab-col-title" th:text="${lab.title}">—</span>
|
<span class="lab-col-title" th:text="${lab.title}">—</span>
|
||||||
<div class="lab-head-actions">
|
<div class="lab-head-actions">
|
||||||
<button class="mini-btn" type="button"
|
<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}|"
|
th:attr="data-url=@{/admin/groups/{id}/assign-modal(id=${group.id}, labId=${lab.id})},data-lab-name=|ЛР ${lab.number}|" data-action="assign"
|
||||||
onclick="openLabAssign(this)"
|
|
||||||
title="Выдача вариантов: уровни, варианты, автокраспределение по группе">
|
title="Выдача вариантов: уровни, варианты, автокраспределение по группе">
|
||||||
<svg class="cell-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
<svg class="cell-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -89,16 +88,14 @@
|
|||||||
<div class="cell-main">
|
<div class="cell-main">
|
||||||
<button class="variant-btn" type="button"
|
<button class="variant-btn" type="button"
|
||||||
th:classappend="${cell.variantManual} ? 'manual' : ''"
|
th:classappend="${cell.variantManual} ? 'manual' : ''"
|
||||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-variant=${cell.variantId}"
|
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-variant=${cell.variantId}" data-action="variant"
|
||||||
onclick="openVariant(this)"
|
|
||||||
title="Назначить вариант вручную или выдать случайный">
|
title="Назначить вариант вручную или выдать случайный">
|
||||||
<span th:if="${cell.variantNumber}" th:text="'В-' + ${cell.variantNumber}">В-1</span>
|
<span th:if="${cell.variantNumber}" th:text="'В-' + ${cell.variantNumber}">В-1</span>
|
||||||
<span th:if="${cell.variantNumber == null}" class="cell-none">выдать</span>
|
<span th:if="${cell.variantNumber == null}" class="cell-none">выдать</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="score-link" type="button"
|
<button class="score-link" type="button"
|
||||||
th:classappend="${cell.score == null} ? 'none' : ''"
|
th:classappend="${cell.score == null} ? 'none' : ''"
|
||||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}"
|
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
|
||||||
onclick="openGradeEditor(this)"
|
|
||||||
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Оценки нет — выставить балл и комментарии'">
|
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Оценки нет — выставить балл и комментарии'">
|
||||||
<span th:text="${cell.score} ?: '—'">—</span>
|
<span th:text="${cell.score} ?: '—'">—</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -113,8 +110,7 @@
|
|||||||
<span th:unless="${cell.submissionId}" class="sub-dim">не сдано</span>
|
<span th:unless="${cell.submissionId}" class="sub-dim">не сдано</span>
|
||||||
<button class="cell-btn st js-defense-cell" type="button"
|
<button class="cell-btn st js-defense-cell" type="button"
|
||||||
th:classappend="${cell.defenseStatus == null} ? 'dim' : (${cell.defenseStatus == 'ready'} ? 'ok' : 'warn')"
|
th:classappend="${cell.defenseStatus == null} ? 'dim' : (${cell.defenseStatus == 'ready'} ? 'ok' : 'warn')"
|
||||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus},data-seconds=${cell.defenseSecondsLeft},data-code=${cell.defenseCode},data-reason=${cell.defenseReadyReason}"
|
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus},data-seconds=${cell.defenseSecondsLeft},data-code=${cell.defenseCode},data-reason=${cell.defenseReadyReason}" data-action="defense"
|
||||||
onclick="openDefense(this)"
|
|
||||||
th:title="${cell.defenseStatus == null} ? 'Защита не назначена — нажмите, чтобы задать задание' : 'Защита: задание и действия'">
|
th:title="${cell.defenseStatus == null} ? 'Защита не назначена — нажмите, чтобы задать задание' : 'Защита: задание и действия'">
|
||||||
<span class="cell-dim">защита:</span>
|
<span class="cell-dim">защита:</span>
|
||||||
<span class="js-def-label"
|
<span class="js-def-label"
|
||||||
@@ -132,15 +128,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right actions-cell">
|
<td class="text-right actions-cell">
|
||||||
<div class="actions-col">
|
<div class="actions-col">
|
||||||
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post"
|
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post" data-confirm="Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.">
|
||||||
onsubmit="return confirm('Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<button class="btn btn-secondary btn-sm" type="submit"
|
<button class="btn btn-secondary btn-sm" type="submit"
|
||||||
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить</button>
|
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить</button>
|
||||||
</form>
|
</form>
|
||||||
<form th:action="@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
|
<form th:action="@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
|
||||||
method="post"
|
method="post" data-confirm="Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.">
|
||||||
onsubmit="return confirm('Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<button class="btn btn-sm btn-danger" type="submit">Отчислить</button>
|
<button class="btn btn-sm btn-danger" type="submit">Отчислить</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -157,7 +151,7 @@
|
|||||||
<dialog id="modal-grade" class="modal modal-lg">
|
<dialog id="modal-grade" class="modal modal-lg">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
|
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-grade')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
|
<form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
|
||||||
@@ -170,7 +164,7 @@
|
|||||||
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
|
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
|
||||||
<div id="cs-rows"></div>
|
<div id="cs-rows"></div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="btn btn-secondary" type="button" onclick="csRow('','neutral')">+ комментарий</button>
|
<button class="btn btn-secondary" type="button" data-action="comment-add">+ комментарий</button>
|
||||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -182,7 +176,7 @@
|
|||||||
<dialog id="modal-variant" class="modal modal-lg">
|
<dialog id="modal-variant" class="modal modal-lg">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Назначить вариант</h2>
|
<h2 class="modal-title">Назначить вариант</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-variant')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body form-stack">
|
<div class="modal-body form-stack">
|
||||||
<p class="text-sm text-muted" id="variant-who">—</p>
|
<p class="text-sm text-muted" id="variant-who">—</p>
|
||||||
@@ -218,7 +212,7 @@
|
|||||||
<dialog id="modal-defense" class="modal">
|
<dialog id="modal-defense" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Индивидуальное задание</h2>
|
<h2 class="modal-title">Индивидуальное задание</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-defense')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post">
|
<form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post">
|
||||||
@@ -247,8 +241,7 @@
|
|||||||
<input type="hidden" name="groupId" th:value="${group.id}">
|
<input type="hidden" name="groupId" th:value="${group.id}">
|
||||||
<button class="btn btn-secondary" type="submit">Выдать код</button>
|
<button class="btn btn-secondary" type="submit">Выдать код</button>
|
||||||
</form>
|
</form>
|
||||||
<form class="inline-form" id="defense-reset-form" method="post"
|
<form class="inline-form" id="defense-reset-form" method="post" data-confirm="Открыть защиту заново для нового (дополнительного) задания?">
|
||||||
onsubmit="return confirm('Открыть защиту заново для нового (дополнительного) задания?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="labId" id="defense-reset-lab">
|
<input type="hidden" name="labId" id="defense-reset-lab">
|
||||||
<input type="hidden" name="groupId" th:value="${group.id}">
|
<input type="hidden" name="groupId" th:value="${group.id}">
|
||||||
@@ -267,7 +260,7 @@
|
|||||||
<dialog id="modal-add-student" class="modal">
|
<dialog id="modal-add-student" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Добавить студента</h2>
|
<h2 class="modal-title">Добавить студента</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-add-student')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" id="enroll-form" method="post"
|
<form class="form-stack" id="enroll-form" method="post"
|
||||||
@@ -346,7 +339,7 @@
|
|||||||
<dialog id="modal-lab-assign" class="modal modal-lg">
|
<dialog id="modal-lab-assign" class="modal modal-lg">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="assign-modal-title">Выдача вариантов</h2>
|
<h2 class="modal-title" id="assign-modal-title">Выдача вариантов</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-lab-assign')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="assign-modal-body"></div>
|
<div class="modal-body" id="assign-modal-body"></div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input class="input table-search" data-target="groups-table"
|
<input class="input table-search" data-target="groups-table"
|
||||||
placeholder="Поиск по названию…" aria-label="Поиск по группам">
|
placeholder="Поиск по названию…" aria-label="Поиск по группам">
|
||||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import-group')">Импорт</button>
|
<button class="btn btn-secondary" type="button" data-modal="modal-import-group">Импорт</button>
|
||||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-group')">+ Новая группа</button>
|
<button class="btn btn-primary" type="button" data-modal="modal-create-group">+ Новая группа</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<dialog id="modal-create-group" class="modal">
|
<dialog id="modal-create-group" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новая группа</h2>
|
<h2 class="modal-title">Новая группа</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-create-group')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/groups}" method="post">
|
<form class="form-stack" th:action="@{/admin/groups}" method="post">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<dialog id="modal-import-group" class="modal">
|
<dialog id="modal-import-group" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Импорт студентов</h2>
|
<h2 class="modal-title">Импорт студентов</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import-group')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
|
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
|
||||||
|
|||||||
@@ -46,8 +46,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<button class="btn btn-danger" type="submit" name="action"
|
<button class="btn btn-danger" type="submit" name="action"
|
||||||
th:value="|delete-level-${level.id}|"
|
th:value="|delete-level-${level.id}|" data-confirm="Удалить уровень? Несохранённые правки остальных полей формы будут потеряны. Варианты на уровне должны быть пустыми.">
|
||||||
onclick="return confirm('Удалить уровень? Несохранённые правки остальных полей формы будут потеряны. Варианты на уровне должны быть пустыми.')">
|
|
||||||
удалить</button>
|
удалить</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -82,8 +81,7 @@
|
|||||||
<div><label class="label">Условие (markdown)</label>
|
<div><label class="label">Условие (markdown)</label>
|
||||||
<textarea class="input" name="variantStatements" rows="6" th:text="${v.statementMd}"></textarea></div>
|
<textarea class="input" name="variantStatements" rows="6" th:text="${v.statementMd}"></textarea></div>
|
||||||
<button class="btn btn-danger" type="submit" name="action"
|
<button class="btn btn-danger" type="submit" name="action"
|
||||||
th:value="|delete-variant-${v.id}|"
|
th:value="|delete-variant-${v.id}|" data-confirm="Удалить вариант? Несохранённые правки остальных полей формы будут потеряны.">Удалить вариант</button>
|
||||||
onclick="return confirm('Удалить вариант? Несохранённые правки остальных полей формы будут потеряны.')">Удалить вариант</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
Статус: <span class="badge"
|
Статус: <span class="badge"
|
||||||
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
|
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
|
||||||
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">draft</span>
|
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">draft</span>
|
||||||
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
|
||||||
onsubmit="return confirm('Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<button class="btn btn-secondary" type="submit"
|
<button class="btn btn-secondary" type="submit"
|
||||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">Опубликовать</button>
|
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">Опубликовать</button>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input class="input table-search" data-target="labs-table"
|
<input class="input table-search" data-target="labs-table"
|
||||||
placeholder="Поиск по названию…" aria-label="Поиск по лабораторным">
|
placeholder="Поиск по названию…" aria-label="Поиск по лабораторным">
|
||||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import-lab')">Импорт</button>
|
<button class="btn btn-secondary" type="button" data-modal="modal-import-lab">Импорт</button>
|
||||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-lab')">+ Создать</button>
|
<button class="btn btn-primary" type="button" data-modal="modal-create-lab">+ Создать</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,8 +27,7 @@
|
|||||||
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">—</span>
|
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post"
|
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
|
||||||
onsubmit="return confirm('Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="@{/admin/labs}">
|
<input type="hidden" name="back" th:value="@{/admin/labs}">
|
||||||
<button class="btn"
|
<button class="btn"
|
||||||
@@ -37,8 +36,7 @@
|
|||||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">—</button>
|
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">—</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post"
|
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" data-confirm="Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо."><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger" type="submit">Удалить</button></form></td>
|
||||||
onsubmit="return confirm('Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо.')"><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger" type="submit">Удалить</button></form></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
|
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -49,7 +47,7 @@
|
|||||||
<dialog id="modal-import-lab" class="modal">
|
<dialog id="modal-import-lab" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Импорт лабораторной</h2>
|
<h2 class="modal-title">Импорт лабораторной</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import-lab')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
|
<form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
|
||||||
@@ -72,7 +70,7 @@
|
|||||||
<dialog id="modal-create-lab" class="modal">
|
<dialog id="modal-create-lab" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новая лабораторная</h2>
|
<h2 class="modal-title">Новая лабораторная</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-create-lab')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/labs/create}" method="post">
|
<form class="form-stack" th:action="@{/admin/labs/create}" method="post">
|
||||||
|
|||||||
@@ -30,8 +30,7 @@
|
|||||||
<td class="font-mono" th:text="${u.studentNo}">—</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><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">
|
<td class="text-right">
|
||||||
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post" data-confirm="Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.">
|
||||||
onsubmit="return confirm('Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.');">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="q" th:value="${query}">
|
<input type="hidden" name="q" th:value="${query}">
|
||||||
<button class="btn btn-danger" type="submit">Удалить</button>
|
<button class="btn btn-danger" type="submit">Удалить</button>
|
||||||
|
|||||||
@@ -21,86 +21,11 @@
|
|||||||
<script th:src="@{/js/highlight.min.js(v='10')}" defer></script>
|
<script th:src="@{/js/highlight.min.js(v='10')}" defer></script>
|
||||||
<script th:src="@{/js/table-scroll.js(v='10')}" defer></script>
|
<script th:src="@{/js/table-scroll.js(v='10')}" defer></script>
|
||||||
<script th:src="@{/js/tablekit.js(v='10')}" defer></script>
|
<script th:src="@{/js/tablekit.js(v='10')}" defer></script>
|
||||||
|
<script th:src="@{/js/theme-init.js(v='10')}"></script>
|
||||||
|
<script th:src="@{/js/app.js(v='10')}" defer></script>
|
||||||
<script th:src="@{/js/ui-state.js(v='10')}" defer></script>
|
<script th:src="@{/js/ui-state.js(v='10')}" defer></script>
|
||||||
<script th:src="@{/js/md-content.js(v='10')}" defer></script>
|
<script th:src="@{/js/md-content.js(v='10')}" defer></script>
|
||||||
<script th:src="@{/js/submit-guard.js(v='10')}" defer></script>
|
<script th:src="@{/js/submit-guard.js(v='10')}" defer></script>
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var t = localStorage.getItem('lms-theme') || 'light';
|
|
||||||
document.documentElement.setAttribute('data-theme', t);
|
|
||||||
window.__hljsSet = function(theme){
|
|
||||||
var l = document.getElementById('hljs-light'), d = document.getElementById('hljs-dark');
|
|
||||||
if (l && d) { l.disabled = theme !== 'light'; d.disabled = theme !== 'dark'; }
|
|
||||||
};
|
|
||||||
window.__hljsAll = function(){
|
|
||||||
if (!window.hljs) return;
|
|
||||||
document.querySelectorAll('pre code').forEach(function(el){ window.hljs.highlightElement(el); });
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
document.addEventListener('DOMContentLoaded', function(){ window.__hljsSet(document.documentElement.getAttribute('data-theme')); window.__hljsAll(); syncModalScrollLock(); initNavActive(); initSections(); });
|
|
||||||
/* Активный пункт меню — как в макете: бордовый цвет у текущего раздела. */
|
|
||||||
function initNavActive(){
|
|
||||||
var path = window.location.pathname;
|
|
||||||
document.querySelectorAll('#top-nav .nav-link').forEach(function (a) {
|
|
||||||
var href = a.getAttribute('href') || '';
|
|
||||||
if (path === href || (href !== '/' && path.indexOf(href + '/') === 0)) a.classList.add('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function toggleTheme(){
|
|
||||||
var e=document.documentElement; var t=e.getAttribute('data-theme')==='dark'?'light':'dark';
|
|
||||||
e.setAttribute('data-theme',t); localStorage.setItem('lms-theme',t); window.__hljsSet(t);
|
|
||||||
}
|
|
||||||
function openModal(id){ var d=document.getElementById(id); if(d) d.showModal(); syncModalScrollLock(); }
|
|
||||||
function closeModal(id){ var d=document.getElementById(id); if(d) d.close(); syncModalScrollLock(); }
|
|
||||||
function openModalData(btn){ openModal(btn.dataset.modal); }
|
|
||||||
function closeModalData(btn){ closeModal(btn.dataset.modal); }
|
|
||||||
/* Пока открыта хоть одна модалка — блокируем прокрутку страницы, сохраняя позицию скролла
|
|
||||||
(внутри .modal-body прокрутка остаётся). */
|
|
||||||
var modalScrollY = 0;
|
|
||||||
function syncModalScrollLock(){
|
|
||||||
var root = document.documentElement;
|
|
||||||
if (document.querySelector('dialog[open]')) {
|
|
||||||
if (root.classList.contains('modal-open')) return;
|
|
||||||
modalScrollY = window.scrollY || window.pageYOffset || 0;
|
|
||||||
var sw = window.innerWidth - root.clientWidth;
|
|
||||||
root.style.setProperty('--modal-scrollbar', (sw > 0 ? sw : 0) + 'px');
|
|
||||||
root.style.setProperty('--modal-top', (-modalScrollY) + 'px');
|
|
||||||
root.classList.add('modal-open');
|
|
||||||
} else if (root.classList.contains('modal-open')) {
|
|
||||||
root.classList.remove('modal-open');
|
|
||||||
root.style.removeProperty('--modal-scrollbar');
|
|
||||||
root.style.removeProperty('--modal-top');
|
|
||||||
window.scrollTo(0, modalScrollY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('toggle', function (e) {
|
|
||||||
if (e.target && e.target.tagName === 'DIALOG') syncModalScrollLock();
|
|
||||||
}, true);
|
|
||||||
function sectionStateKey(id){ return 'lms-section-open:' + id; }
|
|
||||||
function applySectionState(acc){
|
|
||||||
var id = acc.getAttribute('data-section-id');
|
|
||||||
var stored = null;
|
|
||||||
try { stored = id ? localStorage.getItem(sectionStateKey(id)) : null; } catch (e) { /* приватный режим */ }
|
|
||||||
var open = stored === '1' ? true
|
|
||||||
: stored === '0' ? false
|
|
||||||
: acc.getAttribute('data-default-open') === 'true';
|
|
||||||
acc.classList.toggle('collapsed', !open);
|
|
||||||
var btn = acc.querySelector('.sub-toggle');
|
|
||||||
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
||||||
}
|
|
||||||
function initSections(){
|
|
||||||
document.querySelectorAll('.sub-acc[data-section-id]').forEach(applySectionState);
|
|
||||||
}
|
|
||||||
function toggleSection(btn){
|
|
||||||
var acc = btn.closest('.sub-acc');
|
|
||||||
if (!acc) return;
|
|
||||||
var collapsed = acc.classList.toggle('collapsed');
|
|
||||||
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
||||||
var id = acc.getAttribute('data-section-id');
|
|
||||||
if (id) { try { localStorage.setItem(sectionStateKey(id), collapsed ? '0' : '1'); } catch (e) { /* приватный режим */ } }
|
|
||||||
}
|
|
||||||
document.addEventListener('DOMContentLoaded', initSections);
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@@ -122,8 +47,8 @@
|
|||||||
<a th:href="@{/admin}" th:if="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
<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>
|
<a th:href="@{/student}" th:unless="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||||||
</th:block>
|
</th:block>
|
||||||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn btn-secondary btn-sm">Войти</a>
|
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn">Войти</a>
|
||||||
<button type="button" onclick="toggleTheme()" class="btn btn-secondary btn-sm theme-btn"
|
<button type="button" class="btn btn-secondary theme-btn"
|
||||||
aria-label="Сменить тему" title="Сменить тему">
|
aria-label="Сменить тему" title="Сменить тему">
|
||||||
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
<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"/>
|
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/>
|
||||||
@@ -141,7 +66,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
|
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
|
||||||
<button class="btn btn-secondary btn-sm" type="submit">Выйти</button>
|
<button class="btn" type="submit">Выйти</button>
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,39 +76,38 @@
|
|||||||
<th:block th:replace="${content}" />
|
<th:block th:replace="${content}" />
|
||||||
</main>
|
</main>
|
||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
LMS v0.2.0
|
LMS v1.1.0
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-host" id="toast-host" aria-live="polite">
|
<div class="toast-host" id="toast-host" aria-live="polite">
|
||||||
<div th:if="${notice}" class="toast-item toast-success" data-toast>
|
<div th:if="${notice}" class="toast-item toast-success" data-toast>
|
||||||
<span class="toast-text" th:text="${notice}">…</span>
|
<span class="toast-text" th:text="${notice}">…</span>
|
||||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${error}" class="toast-item toast-error" data-toast>
|
<div th:if="${error}" class="toast-item toast-error" data-toast>
|
||||||
<span class="toast-text" th:text="${error}">…</span>
|
<span class="toast-text" th:text="${error}">…</span>
|
||||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${resetPassword}" class="toast-item toast-warning" data-toast>
|
<div th:if="${resetPassword}" class="toast-item toast-warning" data-toast>
|
||||||
<span class="toast-text">
|
<span class="toast-text">
|
||||||
Новый пароль<span th:if="${resetFor}" th:text="| для ${resetFor}|"> для Иванов И.</span>:
|
Новый пароль<span th:if="${resetFor}" th:text="| для ${resetFor}|"> для Иванов И.</span>:
|
||||||
<code class="code font-mono" th:text="${resetPassword}">password</code>
|
<code class="code font-mono" th:text="${resetPassword}">password</code>
|
||||||
</span>
|
</span>
|
||||||
<button class="toast-close" type="button" aria-label="Закрыть" onclick="this.parentElement.remove()">✕</button>
|
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<dialog id="modal-confirm" class="modal">
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
<div class="modal-header">
|
||||||
document.querySelectorAll('[data-toast]').forEach(function (toast) {
|
<h2 class="modal-title" id="confirm-title">Подтверждение</h2>
|
||||||
var timer = setTimeout(function () { toast.remove(); }, 30000);
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
var close = toast.querySelector('.toast-close');
|
</div>
|
||||||
if (close) {
|
<div class="modal-body">
|
||||||
close.addEventListener('click', function () {
|
<p class="confirm-text" id="confirm-text">—</p>
|
||||||
clearTimeout(timer);
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
toast.remove();
|
<button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button>
|
||||||
});
|
<button class="btn btn-primary" type="button" id="confirm-ok">ОК</button>
|
||||||
}
|
</div>
|
||||||
});
|
</div>
|
||||||
});
|
</dialog>
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
||||||
<button class="btn btn-primary" type="button" onclick="startArticleEdit()">Править</button>
|
<button class="btn btn-primary" type="button" data-action="article-edit">Править</button>
|
||||||
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
|
<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="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="id" th:value="${article.id}">
|
<input type="hidden" name="id" th:value="${article.id}">
|
||||||
@@ -57,8 +57,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<a class="btn btn-secondary"
|
<a class="btn btn-secondary"
|
||||||
th:href="@{/learn/{c}(c=${article.chapterSlug})}">К главе</a>
|
th:href="@{/learn/{c}(c=${article.chapterSlug})}">К главе</a>
|
||||||
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
|
||||||
onsubmit="return confirm('Удалить статью?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="${backParent}">
|
<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="Удалить статью"><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>
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
<div class="md-editor-head">
|
<div class="md-editor-head">
|
||||||
<span class="md-editor-title">Редактирование статьи</span>
|
<span class="md-editor-title">Редактирование статьи</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="btn btn-secondary" type="button" onclick="cancelArticleEdit()">Отмена</button>
|
<button class="btn btn-secondary" type="button" data-action="article-edit-cancel">Отмена</button>
|
||||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
||||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-new-section')">+ Раздел</button>
|
<button class="btn btn-primary" type="button" data-modal="modal-new-section">+ Раздел</button>
|
||||||
<button class="mini-btn" type="button" onclick="openModal('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="Переименовать главу"><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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/chapters/toggle-published}" method="post">
|
<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="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="id" th:value="${chapter.id}">
|
<input type="hidden" name="id" th:value="${chapter.id}">
|
||||||
@@ -25,8 +25,7 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post" data-confirm="Удалить главу со всеми разделами и статьями?">
|
||||||
onsubmit="return confirm('Удалить главу со всеми разделами и статьями?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="${backParent}">
|
<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="Удалить главу со всеми разделами и статьями"><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>
|
||||||
@@ -52,7 +51,7 @@
|
|||||||
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
|
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
|
||||||
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
|
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
|
||||||
title="Перетащите мышью: изменить порядок разделов"><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></span>
|
title="Перетащите мышью: изменить порядок разделов"><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></span>
|
||||||
<button type="button" class="sub-toggle" aria-expanded="false" onclick="toggleSection(this)"
|
<button type="button" class="sub-toggle" aria-expanded="false"
|
||||||
title="Свернуть или развернуть раздел">
|
title="Свернуть или развернуть раздел">
|
||||||
<span class="sub-arrow" aria-hidden="true"></span>
|
<span class="sub-arrow" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
th:text="${section.title}">Раздел</a>
|
th:text="${section.title}">Раздел</a>
|
||||||
<span th:if="${!section.published}" class="badge badge-warn">скрыто</span>
|
<span th:if="${!section.published}" class="badge badge-warn">скрыто</span>
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
|
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
|
||||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)" 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: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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
|
<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="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="id" th:value="${section.id}">
|
<input type="hidden" name="id" th:value="${section.id}">
|
||||||
@@ -70,9 +69,8 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<button class="mini-btn" type="button" th:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)" 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: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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел вместе со статьями?">
|
||||||
onsubmit="return confirm('Удалить раздел вместе со статьями?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="${back}">
|
<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="Удалить раздел вместе со статьями"><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>
|
||||||
@@ -112,7 +110,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Переименовать раздел</h2>
|
<h2 class="modal-title">Переименовать раздел</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||||
th:data-modal="|modal-rename-section-${section.id}|" onclick="closeModalData(this)">✕</button>
|
th:data-modal="|modal-rename-section-${section.id}|">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||||
@@ -128,7 +126,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||||
th:data-modal="|modal-move-${section.id}|" onclick="closeModalData(this)">✕</button>
|
th:data-modal="|modal-move-${section.id}|">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||||
@@ -155,7 +153,7 @@
|
|||||||
<dialog id="modal-rename-chapter" class="modal">
|
<dialog id="modal-rename-chapter" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Переименовать главу</h2>
|
<h2 class="modal-title">Переименовать главу</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-rename-chapter')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
||||||
@@ -171,7 +169,7 @@
|
|||||||
<dialog id="modal-new-section" class="modal">
|
<dialog id="modal-new-section" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новый раздел в главе</h2>
|
<h2 class="modal-title">Новый раздел в главе</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-section')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/create}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/create}" method="post">
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<div class="flex items-center justify-between gap-4 mb-6">
|
<div class="flex items-center justify-between gap-4 mb-6">
|
||||||
<h1 class="page-title">Учебные материалы</h1>
|
<h1 class="page-title">Учебные материалы</h1>
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
|
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
|
||||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-new-article')">+ Статья</button>
|
<button class="btn btn-primary" type="button" data-modal="modal-new-article">+ Статья</button>
|
||||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import')">Импорт zip</button>
|
<button class="btn btn-secondary" type="button" data-modal="modal-import">Импорт zip</button>
|
||||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-new-chapter')">+ Глава</button>
|
<button class="btn btn-secondary" type="button" data-modal="modal-new-chapter">+ Глава</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" id="sort-status" class="sort-status" hidden>
|
<div th:if="${currentUser != null and currentUser.admin}" id="sort-status" class="sort-status" hidden>
|
||||||
@@ -35,9 +35,8 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="openModalData(this)" 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: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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post" data-confirm="Удалить главу со всеми разделами и статьями?">
|
||||||
onsubmit="return confirm('Удалить главу со всеми разделами и статьями?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<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="Удалить главу со всеми разделами и статьями"><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>
|
||||||
</form>
|
</form>
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
|
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
|
||||||
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
|
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
|
||||||
title="Перетащите мышью: изменить порядок или перенести раздел"><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></span>
|
title="Перетащите мышью: изменить порядок или перенести раздел"><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></span>
|
||||||
<button type="button" class="sub-toggle" aria-expanded="false" onclick="toggleSection(this)"
|
<button type="button" class="sub-toggle" aria-expanded="false"
|
||||||
title="Свернуть или развернуть раздел">
|
title="Свернуть или развернуть раздел">
|
||||||
<span class="sub-arrow" aria-hidden="true"></span>
|
<span class="sub-arrow" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -69,10 +68,9 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)" 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: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:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)" 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: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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел со всеми статьями?">
|
||||||
onsubmit="return confirm('Удалить раздел со всеми статьями?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<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="Удалить раздел со всеми статьями"><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>
|
||||||
</form>
|
</form>
|
||||||
@@ -99,8 +97,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<a class="btn btn-secondary"
|
<a class="btn btn-secondary"
|
||||||
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
|
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
|
||||||
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
|
||||||
onsubmit="return confirm('Удалить статью?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<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="Удалить статью"><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>
|
||||||
</form>
|
</form>
|
||||||
@@ -115,7 +112,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Переименовать раздел</h2>
|
<h2 class="modal-title">Переименовать раздел</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||||
th:data-modal="|modal-rename-section-${section.id}|" onclick="closeModalData(this)">✕</button>
|
th:data-modal="|modal-rename-section-${section.id}|">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||||
@@ -130,7 +127,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||||
th:data-modal="|modal-move-${section.id}|" onclick="closeModalData(this)">✕</button>
|
th:data-modal="|modal-move-${section.id}|">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||||
@@ -155,7 +152,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Переименовать главу</h2>
|
<h2 class="modal-title">Переименовать главу</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||||
th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="closeModalData(this)">✕</button>
|
th:data-modal="|modal-rename-chapter-${chapter.id}|">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
||||||
@@ -172,7 +169,7 @@
|
|||||||
<dialog id="modal-import" class="modal">
|
<dialog id="modal-import" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Импорт материалов (zip)</h2>
|
<h2 class="modal-title">Импорт материалов (zip)</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
|
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
|
||||||
@@ -198,7 +195,7 @@
|
|||||||
<dialog id="modal-new-chapter" class="modal">
|
<dialog id="modal-new-chapter" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новая глава</h2>
|
<h2 class="modal-title">Новая глава</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-chapter')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
|
||||||
@@ -213,7 +210,7 @@
|
|||||||
<dialog id="modal-new-article" class="modal">
|
<dialog id="modal-new-article" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новая статья</h2>
|
<h2 class="modal-title">Новая статья</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-article')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
|
||||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-new-article')">+ Статья</button>
|
<button class="btn btn-primary" type="button" data-modal="modal-new-article">+ Статья</button>
|
||||||
<button class="mini-btn" type="button" onclick="openModal('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="Переименовать раздел"><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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
|
<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="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="id" th:value="${section.id}">
|
<input type="hidden" name="id" th:value="${section.id}">
|
||||||
@@ -27,9 +27,8 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<button class="mini-btn" type="button" onclick="openModal('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="Перенести статьи и удалить"><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>
|
||||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел вместе со статьями?">
|
||||||
onsubmit="return confirm('Удалить раздел вместе со статьями?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="${backParent}">
|
<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="Удалить раздел вместе со статьями"><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>
|
||||||
@@ -67,8 +66,7 @@
|
|||||||
<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: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: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>
|
||||||
</form>
|
</form>
|
||||||
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
|
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post" data-confirm="Удалить статью?">
|
||||||
onsubmit="return confirm('Удалить статью?')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<input type="hidden" name="back" th:value="${back}">
|
<input type="hidden" name="back" th:value="${back}">
|
||||||
<button class="btn btn-danger" type="submit">Удалить</button>
|
<button class="btn btn-danger" type="submit">Удалить</button>
|
||||||
@@ -83,7 +81,7 @@
|
|||||||
<dialog id="modal-rename-section" class="modal">
|
<dialog id="modal-rename-section" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Переименовать раздел</h2>
|
<h2 class="modal-title">Переименовать раздел</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-rename-section')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||||
@@ -99,7 +97,7 @@
|
|||||||
<dialog id="modal-move-section" class="modal">
|
<dialog id="modal-move-section" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Перенести статьи и удалить раздел</h2>
|
<h2 class="modal-title">Перенести статьи и удалить раздел</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-move-section')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||||
@@ -123,7 +121,7 @@
|
|||||||
<dialog id="modal-new-article" class="modal">
|
<dialog id="modal-new-article" class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title">Новая статья в разделе</h2>
|
<h2 class="modal-title">Новая статья в разделе</h2>
|
||||||
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-article')">✕</button>
|
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<h1 class="page-title" th:text="|Лабораторная работа ${lab.number}. ${lab.title}|">ЛР</h1>
|
<h1 class="page-title" th:text="|Лабораторная работа ${lab.number}. ${lab.title}|">ЛР</h1>
|
||||||
<span th:if="${assignment != null}" class="badge badge-warn">Вариант <span class="font-mono" th:text="${assignment.variantNumber}"></span></span>
|
<span th:if="${assignment != null}" class="badge badge-warn">Вариант <span class="font-mono" th:text="${assignment.variantNumber}"></span></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted mb-6">
|
<p class="text-muted mb-6">
|
||||||
<span th:if="${deadlineMsk}">Срок сдачи: <span th:text="${deadlineMsk}"></span> МСК</span>
|
<span th:if="${deadlineMsk}"><b>Срок сдачи кода:</b> <span th:text="${deadlineMsk}"></span> МСК</span>
|
||||||
<span th:if="${deadlineMsk != null and defenseMsk != null}" class="nav-sep">·</span>
|
<span th:if="${deadlineMsk != null and defenseMsk != null}" class="nav-sep">·</span>
|
||||||
<span th:if="${defenseMsk}">Защита: <b th:text="${defenseMsk}"></b> МСК</span>
|
<span th:if="${defenseMsk}"><b>Защита:</b> <span th:text="${deadlineMsk}"></span> МСК</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="section-title">Условие лабораторной</h2>
|
<h2 class="section-title">Условие лабораторной</h2>
|
||||||
@@ -98,8 +98,7 @@
|
|||||||
<p class="text-sm defense-timer" th:attr="data-seconds=${defense.secondsLeft},data-lab=${lab.id}"></p>
|
<p class="text-sm defense-timer" th:attr="data-seconds=${defense.secondsLeft},data-lab=${lab.id}"></p>
|
||||||
<h3 class="group-title">Ваше индивидуальное задание</h3>
|
<h3 class="group-title">Ваше индивидуальное задание</h3>
|
||||||
<div class="md-body" th:utext="${defense.assignmentHtml}"></div>
|
<div class="md-body" th:utext="${defense.assignmentHtml}"></div>
|
||||||
<form class="mt-4" th:action="'/student/defenses/' + ${defense.id} + '/finish?labId=' + ${lab.id}" method="post"
|
<form class="mt-4" th:action="'/student/defenses/' + ${defense.id} + '/finish?labId=' + ${lab.id}" method="post" data-confirm="Завершить защиту? Продолжить её будет нельзя — дождитесь преподавателя.">
|
||||||
onsubmit="return confirm('Завершить защиту? Продолжить её будет нельзя — дождитесь преподавателя.')">
|
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
<button class="btn btn-primary" type="submit">Готов к сдаче</button>
|
<button class="btn btn-primary" type="submit">Готов к сдаче</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user