You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
9.8 KiB
226 lines
9.8 KiB
{% extends "base.html" %} |
|
{% block title %}Тесты — каталог{% endblock %} |
|
|
|
{% block content %} |
|
{% if ui_variant == 'legacy' %} |
|
<section class="legacy-list-shell"> |
|
<h1 class="font-headline legacy-list-title">Тесты</h1> |
|
<div class="legacy-list-toolbar"> |
|
<button id="btn-create-test" class="btn btn-ghost" type="button"> |
|
Создать |
|
</button> |
|
</div> |
|
|
|
{% if visible %} |
|
<ul class="list-stack" aria-label="Тесты в общем списке"> |
|
{% for t in visible %} |
|
<li class="list-row list-row--split"> |
|
<div class="list-row__main"> |
|
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" class="list-row__link"> |
|
<span class="list-row__title">{{ t.title }}</span> |
|
<span class="list-row__meta"> |
|
{{ t.author_full_name or '—' }} |
|
<span class="list-row__meta-tail"> · Версия {{ t.version }}</span> |
|
</span> |
|
</a> |
|
</div> |
|
<div class="list-row__side"> |
|
<button type="button" class="btn btn-ghost btn-start-pass" data-test-id="{{ t.id }}">Продолжить</button> |
|
</div> |
|
</li> |
|
{% endfor %} |
|
</ul> |
|
{% else %} |
|
<p class="text-muted">Нет тестов</p> |
|
{% endif %} |
|
|
|
{% if hidden %} |
|
<h2 class="font-headline legacy-list-subtitle">Скрытые вами из списка</h2> |
|
<ul class="list-stack" aria-label="Скрытые тесты автора"> |
|
{% for t in hidden %} |
|
<li class="list-row list-row--split list-row--hidden"> |
|
<div class="list-row__main"> |
|
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" class="list-row__link"> |
|
<span class="list-row__title">{{ t.title }}</span> |
|
<span class="list-row__meta"> |
|
{{ t.author_full_name or '—' }} |
|
<span class="list-row__meta-tail"> · Версия {{ t.version }} · скрыт</span> |
|
</span> |
|
</a> |
|
</div> |
|
<div class="list-row__side"> |
|
<button type="button" class="btn btn-ghost btn-start-pass" data-test-id="{{ t.id }}">Продолжить</button> |
|
</div> |
|
</li> |
|
{% endfor %} |
|
</ul> |
|
{% endif %} |
|
</section> |
|
{% else %} |
|
<section class="rounded-2xl bg-white shadow-sm border border-ink-300/60 p-4 sm:p-6"> |
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> |
|
<div> |
|
<h1 class="text-xl sm:text-2xl font-semibold">Каталог тестов</h1> |
|
<p class="mt-1 text-sm text-ink-500">Все активные тесты.</p> |
|
</div> |
|
<button id="btn-create-test" |
|
class="inline-flex items-center justify-center gap-2 px-4 py-3 rounded-lg |
|
bg-brand-600 hover:bg-brand-700 text-white font-medium transition |
|
min-h-11 w-full sm:w-auto"> |
|
<span class="material-symbols-outlined text-base">add</span> |
|
Создать тест |
|
</button> |
|
</div> |
|
|
|
{% if visible %} |
|
<ul class="mt-5 grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> |
|
{% for t in visible %} |
|
<li class="rounded-xl border border-ink-300/60 hover:border-brand-300 hover:shadow-sm transition bg-white"> |
|
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" |
|
class="block p-4 active:bg-ink-100/40"> |
|
<div class="flex items-start justify-between gap-2"> |
|
<h3 class="font-semibold text-ink-900 line-clamp-2 min-w-0">{{ t.title }}</h3> |
|
<span class="text-xs text-ink-500 shrink-0 mt-0.5 whitespace-nowrap">Версия {{ t.version }}</span> |
|
</div> |
|
{% if t.description %} |
|
<p class="mt-1 text-sm text-ink-500 line-clamp-3">{{ t.description }}</p> |
|
{% endif %} |
|
<div class="mt-3 flex items-center justify-between gap-2 text-xs text-ink-500"> |
|
<span class="truncate">{{ t.author_full_name or '—' }}</span> |
|
<span class="inline-flex items-center gap-1 text-brand-700"> |
|
<span class="material-symbols-outlined text-sm">edit_note</span> |
|
Открыть |
|
</span> |
|
</div> |
|
</a> |
|
</li> |
|
{% endfor %} |
|
</ul> |
|
{% else %} |
|
<p class="mt-5 text-ink-500 text-sm">Доступных тестов пока нет.</p> |
|
{% endif %} |
|
|
|
{% if hidden %} |
|
<details class="mt-6 rounded-xl border border-ink-300/60 bg-ink-100/50 p-4"> |
|
<summary class="cursor-pointer font-medium text-ink-700"> |
|
Скрытые из каталога ({{ hidden|length }}) |
|
</summary> |
|
<ul class="mt-3 space-y-2"> |
|
{% for t in hidden %} |
|
<li class="flex items-center justify-between gap-2 text-sm"> |
|
<span>{{ t.title }} <span class="text-ink-500">· v{{ t.version }}</span></span> |
|
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" |
|
class="text-brand-700 hover:underline">Открыть</a> |
|
</li> |
|
{% endfor %} |
|
</ul> |
|
</details> |
|
{% endif %} |
|
</section> |
|
{% endif %} |
|
|
|
<dialog id="dlg-create" |
|
class="m-0 p-0 w-full h-full sm:h-auto sm:max-w-md sm:w-full sm:m-auto |
|
sm:rounded-2xl bg-white backdrop:bg-ink-900/50"> |
|
<form method="dialog" class="flex flex-col h-full sm:h-auto bg-white sm:rounded-2xl"> |
|
<div class="px-4 sm:px-5 py-3 border-b border-ink-300/60 flex items-center justify-between"> |
|
<h2 class="text-lg font-semibold">Новый тест</h2> |
|
<button type="button" id="dlg-cancel-x" |
|
class="p-2 rounded hover:bg-ink-100 min-w-10 min-h-10" |
|
aria-label="Закрыть"> |
|
<span class="material-symbols-outlined">close</span> |
|
</button> |
|
</div> |
|
<div class="flex-1 overflow-y-auto px-4 sm:px-5 py-4 space-y-3"> |
|
<label class="block"> |
|
<span class="text-sm font-medium text-ink-700">Название</span> |
|
<input id="new-test-title" type="text" required maxlength="200" |
|
class="mt-1 w-full rounded-lg border border-ink-300 px-3 py-3 |
|
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" /> |
|
</label> |
|
<label class="block"> |
|
<span class="text-sm font-medium text-ink-700">Описание (опц.)</span> |
|
<textarea id="new-test-desc" rows="3" |
|
class="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 |
|
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"></textarea> |
|
</label> |
|
</div> |
|
<div class="px-4 sm:px-5 py-3 border-t border-ink-300/60 flex justify-end gap-2 |
|
bg-ink-100/40 sm:rounded-b-2xl |
|
pb-[max(env(safe-area-inset-bottom),0.75rem)]"> |
|
<button type="button" id="dlg-cancel" |
|
class="px-4 py-2.5 rounded-lg text-ink-700 hover:bg-ink-100 min-h-11">Отмена</button> |
|
<button type="button" id="dlg-submit" |
|
class="px-4 py-2.5 rounded-lg bg-brand-600 hover:bg-brand-700 text-white min-h-11"> |
|
Создать |
|
</button> |
|
</div> |
|
</form> |
|
</dialog> |
|
{% endblock %} |
|
|
|
{% block scripts %} |
|
<script> |
|
(() => { |
|
const dlg = document.getElementById('dlg-create'); |
|
const titleEl = document.getElementById('new-test-title'); |
|
const descEl = document.getElementById('new-test-desc'); |
|
|
|
document.getElementById('btn-create-test').addEventListener('click', () => { |
|
titleEl.value = ''; |
|
descEl.value = ''; |
|
if (typeof dlg.showModal === 'function') dlg.showModal(); |
|
else dlg.setAttribute('open', 'open'); |
|
setTimeout(() => titleEl.focus(), 50); |
|
}); |
|
document.getElementById('dlg-cancel').addEventListener('click', () => dlg.close()); |
|
document.getElementById('dlg-cancel-x').addEventListener('click', () => dlg.close()); |
|
|
|
document.getElementById('dlg-submit').addEventListener('click', async () => { |
|
const title = titleEl.value.trim(); |
|
if (!title) { titleEl.focus(); return; } |
|
try { |
|
const r = await fetch('/api/tests', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ title, description: descEl.value.trim() || null }), |
|
}); |
|
const data = await r.json(); |
|
if (!r.ok) throw new Error(data.error || 'Не удалось создать тест.'); |
|
window.location.href = `/tests/${data.testId}/edit`; |
|
} catch (e) { |
|
alert(e.message || 'Не удалось создать тест.'); |
|
} |
|
}); |
|
|
|
const passButtons = Array.from(document.querySelectorAll('.btn-start-pass')); |
|
passButtons.forEach((btn) => { |
|
btn.addEventListener('click', async () => { |
|
const testId = btn.dataset.testId; |
|
if (!testId) return; |
|
btn.disabled = true; |
|
btn.textContent = 'Продолжить…'; |
|
try { |
|
const r = await fetch(`/api/tests/${testId}/attempts/start`, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({}), |
|
}); |
|
let data = {}; |
|
try { data = await r.json(); } catch (_) {} |
|
if (!r.ok || !data.attempt || !data.attempt.id) { |
|
// В Flask legacy контуре пока может отсутствовать отдельная UI-страница попытки. |
|
// Тогда ведём в карточку теста, чтобы пользователь не попадал на not_found. |
|
window.location.href = `/tests/${testId}/edit`; |
|
return; |
|
} |
|
window.location.href = `/tests/${testId}/attempt/${data.attempt.id}`; |
|
} catch (e) { |
|
window.location.href = `/tests/${testId}/edit`; |
|
return; |
|
} |
|
}); |
|
}); |
|
})(); |
|
</script> |
|
{% endblock %}
|
|
|