- add a generic AJAX submitter for forms inside dialogs - keep modal values and show field errors on server validation failure - soft-refresh the page content on success, preserving scroll, search and filters - share toast and re-init hooks for tables, markdown, sections and group page - require passwords and add minlength/match validation on auth forms
115 lines
7.4 KiB
HTML
115 lines
7.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:fragment="layout(title, content)">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title th:replace="${title}">LMS</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<!-- Inter с запасным системным гротеском. Загрузка не блокирует рендер:
|
||
при недоступном CDN страница открывается на системном шрифте. -->
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||
rel="stylesheet" media="print" onload="this.media='all'">
|
||
<noscript>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
</noscript>
|
||
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='36')}">
|
||
<!-- v=37: сброс кэша стилей после изменения геометрии; при изменении css поднимать версию -->
|
||
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
|
||
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
|
||
<link rel="stylesheet" th:href="@{/css/app.css(v='37')}">
|
||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='36')}">
|
||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='36')}" disabled>
|
||
<script th:src="@{/js/highlight.min.js(v='36')}" defer></script>
|
||
<script th:src="@{/js/table-scroll.js(v='38')}" defer></script>
|
||
<script th:src="@{/js/tablekit.js(v='38')}" defer></script>
|
||
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
|
||
<script th:src="@{/js/theme-init.js(v='36')}"></script>
|
||
<script th:src="@{/js/app.js(v='38')}" defer></script>
|
||
<script th:src="@{/js/ui-state.js(v='36')}" defer></script>
|
||
<script th:src="@{/js/form-feedback.js(v='39')}" defer></script>
|
||
<script th:src="@{/js/modal-ajax.js(v='1')}" defer></script>
|
||
<script th:src="@{/js/md-content.js(v='38')}" defer></script>
|
||
<script th:src="@{/js/submit-guard.js(v='36')}" defer></script>
|
||
</head>
|
||
<body>
|
||
<div class="page d-flex flex-column min-vh-100">
|
||
<nav class="navbar b3-navbar navbar-expand-md sticky-top">
|
||
<div class="container">
|
||
<a th:href="@{/}" class="navbar-brand">LMS<b>.</b></a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileNav"
|
||
aria-controls="mobileNav" aria-label="Меню">
|
||
<i class="bi bi-list"></i>
|
||
</button>
|
||
<div class="offcanvas offcanvas-end app-offcanvas" tabindex="-1" id="mobileNav" aria-labelledby="mobileNavLabel">
|
||
<div class="offcanvas-header">
|
||
<a th:href="@{/learn}" class="navbar-brand" id="mobileNavLabel">LMS<b>.</b></a>
|
||
<button type="button" class="navbar-close" data-bs-dismiss="offcanvas" aria-label="Закрыть">✕</button>
|
||
</div>
|
||
<div class="offcanvas-body">
|
||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/groups}">Группы</a></li>
|
||
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/users}">Пользователи</a></li>
|
||
<li class="nav-item" th:if="${currentUser != null and currentUser.admin}"><a class="nav-link" th:href="@{/admin/labs}">Лабораторные</a></li>
|
||
<li class="nav-item" th:if="${currentUser != null and !currentUser.admin}"><a class="nav-link" th:href="@{/student/labs}">Лабораторные</a></li>
|
||
<li class="nav-item" th:if="${currentUser != null}"><a class="nav-link" th:href="@{/change-password}">Смена пароля</a></li>
|
||
</ul>
|
||
<div class="navbar-user d-flex flex-column flex-md-row align-items-start align-items-md-center gap-2">
|
||
<a th:href="@{/admin}" th:if="${currentUser != null and currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||
<a th:href="@{/student}" th:if="${currentUser != null and !currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<button type="button" class="btn btn-secondary btn-sm theme-btn" aria-label="Сменить тему" title="Сменить тему">
|
||
<i class="bi bi-circle-half"></i>
|
||
</button>
|
||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn btn-sm">Войти</a>
|
||
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post" class="m-0">
|
||
<button class="btn btn-sm" type="submit">Выйти</button>
|
||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<main class="container page-main flex-grow-1">
|
||
<th:block th:replace="${content}" />
|
||
</main>
|
||
<footer class="page-footer mt-auto">
|
||
LMS v1.1.0
|
||
</footer>
|
||
</div>
|
||
<div id="form-feedback" hidden th:if="${formFeedbackJson != null}" th:attr="data-feedback=${formFeedbackJson}"></div>
|
||
<div class="toast-host" id="toast-host" aria-live="polite">
|
||
<div th:if="${notice}" class="toast-item toast-success" data-toast>
|
||
<span class="toast-text" th:text="${notice}">…</span>
|
||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||
</div>
|
||
<div th:if="${error}" class="toast-item toast-error" data-toast>
|
||
<span class="toast-text" th:text="${error}">…</span>
|
||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||
</div>
|
||
<div th:if="${resetPassword}" class="toast-item toast-warning" data-toast>
|
||
<span class="toast-text">
|
||
Новый пароль<span th:if="${resetFor}" th:text="| для ${resetFor}|"> для Иванов И.</span>:
|
||
<code class="code font-mono" th:text="${resetPassword}">password</code>
|
||
</span>
|
||
<button class="toast-close" type="button" aria-label="Закрыть">✕</button>
|
||
</div>
|
||
</div>
|
||
<dialog id="modal-confirm" class="dlg">
|
||
<div class="dlg-header">
|
||
<h2 class="dlg-title" id="confirm-title">Подтверждение</h2>
|
||
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
||
</div>
|
||
<div class="dlg-body">
|
||
<p class="confirm-text" id="confirm-text">—</p>
|
||
<div class="flex justify-end gap-2 mt-4">
|
||
<button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button>
|
||
<button class="btn btn-primary" type="button" id="confirm-ok">ОК</button>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
</body>
|
||
</html>
|