134 lines
4.9 KiB
HTML
134 lines
4.9 KiB
HTML
{#
|
|
Reusable UI macros for Tutortool templates.
|
|
Usage: {% from "macros/ui.html" import flash_messages, styled_field, text_input %}
|
|
#}
|
|
|
|
|
|
{# Renders all flashed messages with colour-coded styling. #}
|
|
{% macro flash_messages() %}
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="space-y-3 mb-6">
|
|
{% for category, message in messages %}
|
|
{% if category == 'error' %}
|
|
{% set alert_class = 'bg-red-900 border-red-400 text-red-100' %}
|
|
{% elif category == 'expired' %}
|
|
{% set alert_class = 'bg-orange-900 border-orange-400 text-orange-100' %}
|
|
{% else %}
|
|
{% set alert_class = 'bg-green-900 border-green-400 text-green-100' %}
|
|
{% endif %}
|
|
<div class="p-3 border rounded-lg {{ alert_class }}" role="alert">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Renders a WTForms field with a label and standard Tailwind styling.
|
|
Pass extra_kwargs as a dict for additional HTML attributes (e.g. type, multiple).
|
|
#}
|
|
{% macro styled_field(field, extra_kwargs={}) %}
|
|
<div>
|
|
<label class="block mb-1 text-sm font-medium">{{ field.label }}</label>
|
|
{{ field(
|
|
class="w-full rounded-lg p-3 bg-background border border-border text-text placeholder-muted focus:outline-none focus:ring-2 focus:ring-primary",
|
|
**extra_kwargs
|
|
) }}
|
|
{% for error in field.errors %}
|
|
<p class="text-red-400 text-sm mt-1">{{ error }}</p>
|
|
{% endfor %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Renders a plain <input> (not WTForms) with standard styling.
|
|
`type` defaults to 'text'.
|
|
#}
|
|
{% macro text_input(name, label, placeholder='', type='text', required=true, autofocus=false) %}
|
|
<div class="mb-4">
|
|
<label for="{{ name }}" class="block text-sm font-medium text-gray-400 mb-2">{{ label }}</label>
|
|
<input
|
|
type="{{ type }}"
|
|
id="{{ name }}"
|
|
name="{{ name }}"
|
|
placeholder="{{ placeholder }}"
|
|
{% if required %}required{% endif %}
|
|
{% if autofocus %}autofocus{% endif %}
|
|
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{# Renders a delete button wrapped in a form. #}
|
|
{% macro delete_button(action_url, label='Delete') %}
|
|
<form method="POST" action="{{ action_url }}">
|
|
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded-lg text-sm">
|
|
{{ label }}
|
|
</button>
|
|
</form>
|
|
{% endmacro %}
|
|
|
|
|
|
{# Course-switcher dropdown (used in admin and index pages). #}
|
|
{% macro course_switcher(courses, active_course_name, switch_url_prefix) %}
|
|
<div class="relative" id="course-switcher">
|
|
<button
|
|
type="button"
|
|
onclick="toggleCourseSwitcher()"
|
|
class="flex items-center gap-2 px-4 py-1.5 rounded-lg border border-border bg-surface
|
|
text-sm font-medium text-text hover:border-primary hover:text-primary transition whitespace-nowrap">
|
|
<span id="course-switcher-label">{{ active_course_name }}</span>
|
|
<svg class="w-3.5 h-3.5 text-muted transition-transform duration-200" id="course-switcher-chevron"
|
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div id="course-switcher-menu"
|
|
class="hidden absolute left-1/2 -translate-x-1/2 mt-2 w-52 z-50
|
|
bg-surface border border-border rounded-xl shadow-lg overflow-hidden whitespace-nowrap">
|
|
{% for course in courses %}
|
|
<form method="POST" action="{{ switch_url_prefix }}/{{ course.id }}">
|
|
<button type="submit"
|
|
class="w-full text-left px-4 py-2.5 text-sm
|
|
{% if course.name == active_course_name %}
|
|
text-primary font-semibold bg-primary/10
|
|
{% else %}
|
|
text-text hover:bg-background hover:text-primary
|
|
{% endif %}
|
|
transition">
|
|
{{ course.name }}
|
|
</button>
|
|
</form>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleCourseSwitcher() {
|
|
const menu = document.getElementById('course-switcher-menu');
|
|
const chevron = document.getElementById('course-switcher-chevron');
|
|
const isOpen = !menu.classList.contains('hidden');
|
|
menu.classList.toggle('hidden', isOpen);
|
|
chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
|
|
|
|
if (!isOpen) {
|
|
// Close when clicking outside
|
|
document.addEventListener('click', function handler(e) {
|
|
if (!document.getElementById('course-switcher').contains(e.target)) {
|
|
menu.classList.add('hidden');
|
|
chevron.style.transform = '';
|
|
document.removeEventListener('click', handler);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endmacro %}
|