Files
LMS/web/src/main/resources/templates/learn/index.html
Vladislav Kopnin 51d1b84297 Redesign B3 bordeaux + finish lab-page rework
- Lab page is now read-only with a settings screen for levels/variants;
  variant issuance moved to a large modal on the group page (assign-fragment).
- Header: bordeaux logo and 2px accent underline, bordeaux login/logout/theme
  buttons; smaller radii across the UI; filled secondary/danger buttons.
- Includes prior agent work (login backoff, no-cache pages, API timeouts,
  content tweaks) and fixes (variant usage Map.values, import order).
- Bump static asset version to v=9.
2026-10-02 15:41:16 +03:00

249 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
<head><title>Материалы — LMS</title></head>
<body>
<div id="content" th:attr="data-csrf=${csrfToken},data-sort-scope='full'">
<div class="flex items-center justify-between gap-4 mb-6">
<h1 class="page-title">Учебные материалы</h1>
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
<button class="btn btn-primary" type="button" onclick="openModal('modal-new-article')">+ Статья</button>
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import')">Импорт zip</button>
<button class="btn btn-secondary" type="button" onclick="openModal('modal-new-chapter')">+ Глава</button>
</div>
</div>
<div th:if="${currentUser != null and currentUser.admin}" id="sort-status" class="sort-status" hidden>
<span id="sort-status-text">Изменения не сохранены…</span>
</div>
<div th:if="${tree == null || #lists.isEmpty(tree.chapters)}" class="card card-body text-muted">
Материалы ещё не добавлены.
</div>
<div th:each="chapter, chStat : ${tree?.chapters}" class="card card-body chapter-card mb-4"
th:attr="data-chapter-id=${chapter.id}">
<div class="sort-row">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='chapter',data-drag-id=${chapter.id}"
title="Перетащите мышью: изменить порядок глав">⠿</span>
<h2 class="section-title sort-title m-0" th:text="${chapter.title}">Глава</h2>
<span th:if="${!chapter.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
<form class="inline-form" th:action="@{/admin/content/chapters/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${chapter.id}">
<input type="hidden" name="published" th:value="${!chapter.published}">
<button class="btn" type="submit"
th:classappend="${chapter.published} ? 'btn-secondary' : 'btn-primary'"
th:text="${chapter.published} ? 'Скрыть главу' : 'Опубликовать главу'">Скрыть главу</button>
</form>
<button class="btn btn-secondary" type="button"
th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="openModalData(this)">Переименовать</button>
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post"
onsubmit="return confirm('Удалить главу со всеми разделами и статьями?')">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-danger" type="submit">Удалить главу</button>
</form>
</div>
</div>
<div data-section-list>
<p th:if="${#lists.isEmpty(chapter.sections)}" class="text-sm text-muted mt-2 mb-0">Разделов пока нет</p>
<div th:each="section : ${chapter.sections}" class="sub-acc collapsed"
th:attr="data-section-id=${section.id},data-default-open=${chStat.last}">
<div class="sub-head sort-row">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='section',data-drag-id=${section.id}"
title="Перетащите мышью: изменить порядок или перенести раздел">⠿</span>
<button type="button" class="sub-toggle" aria-expanded="false" onclick="toggleSection(this)"
title="Свернуть или развернуть раздел">
<span class="sub-arrow" aria-hidden="true"></span>
</button>
<a class="group-title sort-title m-0"
th:href="@{/learn/{c}/{s}(c=${chapter.slug}, s=${section.slug})}"
th:text="${section.title}">Раздел</a>
<span th:if="${!section.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${section.id}">
<input type="hidden" name="published" th:value="${!section.published}">
<button class="btn" type="submit"
th:classappend="${section.published} ? 'btn-secondary' : 'btn-primary'"
th:text="${section.published} ? 'Скрыть раздел' : 'Опубликовать раздел'">Скрыть раздел</button>
</form>
<button class="btn btn-secondary" type="button"
th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)">Переименовать</button>
<button class="btn btn-secondary" type="button"
th:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)">Перенести статьи</button>
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
onsubmit="return confirm('Удалить раздел со всеми статьями?')">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-danger" type="submit">Удалить раздел</button>
</form>
</div>
</div>
<div class="section-body">
<ul class="sort-list text-sm" data-article-list>
<li th:each="article : ${section.articles}" th:attr="data-article-id=${article.id}">
<span th:if="${currentUser != null and currentUser.admin}" class="drag-handle" draggable="true"
th:attr="data-drag-kind='article',data-drag-id=${article.id}"
title="Перетащите мышью: изменить порядок или перенести статью">⠿</span>
<a class="sort-title"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug})}"
th:text="${article.title}">Статья</a>
<span th:if="${!article.published}" class="badge badge-warn">скрыто</span>
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
<input type="hidden" name="published" th:value="${!article.published}">
<button class="btn" type="submit"
th:classappend="${article.published} ? 'btn-secondary' : 'btn-primary'"
th:text="${article.published} ? 'Скрыть' : 'Опубликовать'">Скрыть</button>
</form>
<a class="btn btn-secondary"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
onsubmit="return confirm('Удалить статью?')">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-danger" type="submit">✕</button>
</form>
</div>
</li>
<li th:if="${#lists.isEmpty(section.articles)}" class="text-muted">Статей пока нет</li>
</ul>
</div>
<th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-section-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
th:data-modal="|modal-rename-section-${section.id}|" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label>
<input class="input" name="title" th:value="${section.title}" required></div>
<button class="btn btn-primary" type="submit">Сохранить</button>
</form>
</div>
</dialog>
<dialog th:id="|modal-move-${section.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Перенести статьи из раздела</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
th:data-modal="|modal-move-${section.id}|" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted">Выберите раздел-приёмник: статьи перенесутся туда, а текущий раздел будет удалён.</p>
<div><label class="label">Перенести в раздел</label>
<select class="input" name="moveTo">
<option th:each="pair : ${sectionChoices}"
th:if="${pair['id'] != section.id}"
th:value="${pair['id']}" th:text="${pair['label']}">—</option>
</select></div>
<button class="btn btn-primary" type="submit">Перенести и удалить раздел</button>
</form>
</div>
</dialog>
</th:block>
</div>
</div>
<th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать главу</h2>
<button class="modal-close" type="button" aria-label="Закрыть"
th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label>
<input class="input" name="title" th:value="${chapter.title}" required></div>
<button class="btn btn-primary" type="submit">Сохранить</button>
</form>
</div>
</dialog>
</th:block>
</div>
<dialog id="modal-import" class="modal">
<div class="modal-header">
<h2 class="modal-title">Импорт материалов (zip)</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-import')">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted">
Структура: <code class="code">chapters/01-глава/01-раздел/01-статья.md</code>, картинки в <code class="code">assets/</code>.
</p>
<div><label class="label" for="file">Архив</label>
<input class="input" id="file" name="file" type="file" accept=".zip" required></div>
<label class="tone-label">
<input type="checkbox" name="sync" value="true">
Удалять разделы и статьи, которых нет в архиве (синхронизация)
</label>
<p class="hint">
Без этой галочки импорт <b>только добавляет и обновляет</b> материалы из архива —
статьи, отредактированные прямо в приложении, не затрагиваются.
</p>
<button class="btn btn-primary" type="submit">Импортировать</button>
</form>
</div>
</dialog>
<dialog id="modal-new-chapter" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая глава</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-chapter')">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="chTitle">Название</label>
<input class="input" id="chTitle" name="title" required></div>
<button class="btn btn-primary" type="submit">Создать</button>
</form>
</div>
</dialog>
<dialog id="modal-new-article" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новая статья</h2>
<button class="modal-close" type="button" aria-label="Закрыть" onclick="closeModal('modal-new-article')">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="naChapter">Существующая глава и раздел</label>
<select class="input" id="naChapter" name="sectionId">
<option th:each="pair : ${sectionChoices}"
th:value="${pair['id']}" th:text="${pair['label']}">—</option>
</select></div>
<div><label class="label" for="naNewSection">…или новый раздел в выбранной главе</label>
<input class="input" id="naNewSection" name="newSectionTitle" placeholder="Новый раздел"></div>
<div><label class="label" for="naTitle">Заголовок статьи</label>
<input class="input" id="naTitle" name="title" required></div>
<div><label class="label" for="naBody">Текст (markdown)</label>
<div th:replace="~{fragments/md-toolbar :: toolbar('#naBody')}"></div>
<textarea class="input md-editor-body" id="naBody" name="bodyMd" rows="18"></textarea></div>
<button class="btn btn-primary" type="submit">Создать</button>
</form>
</div>
</dialog>
<script th:src="@{/js/mdeditor.js(v='9')}" defer></script>
<script th:src="@{/js/content-sort.js(v='9')}" defer></script>
</div>
</body>
</html>