Files
tutor-tool/app/templates/macros/ui.html
2026-02-22 01:16:20 +01:00

137 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-full
bg-surface shadow-md ring-1 ring-border
text-sm font-medium text-text
hover:ring-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 %}