refactored templates
This commit is contained in:
133
app/templates/macros/ui.html
Normal file
133
app/templates/macros/ui.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{#
|
||||
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 %}
|
||||
Reference in New Issue
Block a user