This commit is contained in:
cedrick2711
2026-08-25 14:03:07 -04:00
191 changed files with 28790 additions and 5066 deletions
@@ -0,0 +1,19 @@
{# Language switcher.
Included from both branches of the layout: the signed-in sidebar and the
anonymous authentication page. Someone who cannot read the current
language has to be able to change it *before* signing in, so this cannot
live behind the login.
Each language is written in its own language, for the same reason. #}
<i class="fas fa-language" aria-hidden="true"></i>
<span class="sr-only">{{ _('Language') }}</span>
{% for code in supported_locales %}
{%- if code == current_locale %}
<span class="lang-current" aria-current="true">{{ locale_names[code] }}</span>
{%- else %}
<a href="{{ url_for('main.set_language', locale=code) }}"
class="lang-link" hreflang="{{ code }}" rel="alternate">{{ locale_names[code] }}</a>
{%- endif %}
{%- if not loop.last %}<span class="lang-separator" aria-hidden="true">·</span>{% endif %}
{% endfor %}
+41
View File
@@ -0,0 +1,41 @@
{#
Pagination controls (MNT-14).
Import and call:
{% import 'layouts/_pagination.html' as pager %}
{{ pager.controls(pagination) }}
`page_url` is a Jinja global registered in app.py; it rebuilds the current
URL at another page number, keeping the rest of the query string. That is
the part that gets forgotten — dropping `sort` or `team_id` from a
pagination link silently resets the view someone was looking at.
Plain <a> links only: no inline handler, nothing for CSP to refuse
(tests/test_csp.py).
#}
{% macro controls(pagination) %}
{% if pagination.pages > 1 %}
<nav class="pagination" aria-label="{{ _('Pagination') }}">
{% if pagination.has_prev %}
<a class="btn btn-secondary btn-sm" href="{{ page_url(pagination.prev_num) }}"
rel="prev">&laquo; {{ _('Previous') }}</a>
{% else %}
<span class="btn btn-secondary btn-sm is-disabled" aria-disabled="true">&laquo; {{ _('Previous') }}</span>
{% endif %}
<span class="pagination-status">
{{ _('Page %(page)s of %(pages)s', page=pagination.page, pages=pagination.pages) }}
&middot;
{{ _('%(total)s in total', total=pagination.total) }}
</span>
{% if pagination.has_next %}
<a class="btn btn-secondary btn-sm" href="{{ page_url(pagination.next_num) }}"
rel="next">{{ _('Next') }} &raquo;</a>
{% else %}
<span class="btn btn-secondary btn-sm is-disabled" aria-disabled="true">{{ _('Next') }} &raquo;</span>
{% endif %}
</nav>
{% endif %}
{% endmacro %}
+74 -29
View File
@@ -1,10 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ current_locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Team Tryout Management{% endblock %}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<title>{% block title %}UdeS team manager{% endblock %}</title>
{# Subresource integrity (QUA-004). Without it, whoever controls the CDN
controls what runs on every page of this site — and the CSP names these
hosts as allowed, so it would not object.
What SRI does and does not do: it pins this exact file, so the browser
refuses a version that has been altered since. It does not prove the
file was honest when the hash was taken. This hash is the one cdnjs
publishes for the release, not one derived from the copy we downloaded.
integrity requires crossorigin. Changing the version means changing
the hash, or the asset silently stops loading. #}
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏆</text></svg>">
</head>
@@ -13,8 +28,8 @@
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">
<i class="fas fa-trophy"></i>
<span>TryoutPro</span>
<img src="{{ url_for('static', filename='images/UdeS_logo.png') }}" alt="UdeS Logo" class="logo-img">
<span>UdeS team manager</span>
</div>
<div class="user-badge">
<div class="user-avatar">
@@ -30,26 +45,26 @@
<li>
<a href="{{ url_for('main.dashboard') }}" class="{% if request.endpoint and 'dashboard' in request.endpoint %}active{% endif %}">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
<span>{{ _('Dashboard') }}</span>
</a>
</li>
<li>
<a href="{{ url_for('tryouts.list_tryouts') }}" class="{% if request.endpoint and 'tryouts' in request.endpoint and request.endpoint != 'tryouts.create_tryout' %}active{% endif %}">
<i class="fas fa-calendar-alt"></i>
<span>Tryouts</span>
<span>{{ _('Tryouts') }}</span>
</a>
</li>
<li>
<a href="{{ url_for('matches.calendar') }}" class="{% if request.endpoint and 'calendar' in request.endpoint %}active{% endif %}">
<i class="fas fa-calendar"></i>
<span>Calendar</span>
<span>{{ _('Calendar') }}</span>
</a>
</li>
{% if current_user.can_evaluate() %}
<li>
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
<i class="fas fa-clipboard-check"></i>
<span>Evaluations</span>
<span>{{ _('Evaluations') }}</span>
</a>
</li>
{% endif %}
@@ -57,14 +72,14 @@
<li>
<a href="{{ url_for('teams.my_teams') }}" class="{% if request.endpoint == 'teams.my_teams' %}active{% endif %}">
<i class="fas fa-users"></i>
<span>My Team(s)</span>
<span>{{ _('My Team(s)') }}</span>
</a>
</li>
{% else %}
<li>
<a href="{{ url_for('teams.list_teams') }}" class="{% if request.endpoint and 'teams' in request.endpoint and request.endpoint != 'teams.my_teams' %}active{% endif %}">
<i class="fas fa-users-cog"></i>
<span>Manage Teams</span>
<span>{{ _('Manage Teams') }}</span>
</a>
</li>
{% endif %}
@@ -72,7 +87,7 @@
<li>
<a href="{{ url_for('users.list_users') }}" class="{% if request.endpoint and 'users' in request.endpoint and request.endpoint != 'users.profile' %}active{% endif %}">
<i class="fas fa-users-cog"></i>
<span>Manage Users</span>
<span>{{ _('Manage Users') }}</span>
</a>
</li>
<li>
@@ -86,7 +101,7 @@
<li>
<a href="{{ url_for('users.my_notes') }}" class="{% if request.endpoint == 'users.my_notes' %}active{% endif %}">
<i class="fas fa-sticky-note"></i>
<span>My Notes</span>
<span>{{ _('My Notes') }}</span>
</a>
</li>
{% endif %}
@@ -94,42 +109,53 @@
<li>
<a href="{{ url_for('users.notes_dashboard') }}" class="{% if request.endpoint == 'users.notes_dashboard' or request.endpoint == 'users.manage_team_notes' or request.endpoint == 'users.manage_personal_notes' %}active{% endif %}">
<i class="fas fa-sticky-note"></i>
<span>Notes & One on One</span>
<span>{{ _('Notes & One on One') }}</span>
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('users.list_contracts') }}" class="{% if request.endpoint and 'contracts' in request.endpoint %}active{% endif %}">
<i class="fas fa-file-contract"></i>
<span>Contracts</span>
<span>{{ _('Contracts') }}</span>
</a>
</li>
<li class="nav-divider"></li>
<li>
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
<i class="fas fa-user"></i>
<span>My Profile</span>
<span>{{ _('My Profile') }}</span>
</a>
</li>
<li>
<a href="{{ url_for('auth.logout') }}" class="logout-link">
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</a>
{# A form, not a link: logging out is a state change, and a
GET route carries no CSRF token — any site could sign the
user out with an <img> tag. Styled as a nav entry. #}
<form method="POST" action="{{ url_for('auth.logout') }}" class="nav-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="logout-link">
<i class="fas fa-sign-out-alt"></i>
<span>{{ _('Logout') }}</span>
</button>
</form>
</li>
<li class="nav-divider"></li>
<li class="nav-language">
{% include "layouts/_language_switcher.html" %}
</li>
</ul>
</nav>
<div class="main-content" id="mainContent">
<header class="top-bar">
<button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()">
<button class="sidebar-toggle" id="sidebarToggle" data-action="toggle-sidebar">
<i class="fas fa-bars"></i>
</button>
<div class="page-header">
<h1>{% block page_title %}Dashboard{% endblock %}</h1>
<h1>{% block page_title %}{{ _('Dashboard') }}{% endblock %}</h1>
{% block breadcrumb %}{% endblock %}
</div>
<button class="dark-mode-toggle" id="darkModeToggle" onclick="toggleDarkMode()" title="Toggle dark mode">
<button class="dark-mode-toggle" id="darkModeToggle" data-action="toggle-dark-mode"
title="{{ _('Toggle dark mode') }}">
<i class="fas fa-moon"></i>
</button>
{% block header_actions %}{% endblock %}
@@ -140,7 +166,8 @@
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible">
<span>{{ message }}</span>
<button type="button" class="alert-close" onclick="this.parentElement.remove()">&times;</button>
<button type="button" class="alert-close" data-action="dismiss-alert"
aria-label="{{ _('Dismiss') }}">&times;</button>
</div>
{% endfor %}
{% endif %}
@@ -158,7 +185,8 @@
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible">
<span>{{ message }}</span>
<button type="button" class="alert-close" onclick="this.parentElement.remove()">&times;</button>
<button type="button" class="alert-close" data-action="dismiss-alert"
aria-label="{{ _('Dismiss') }}">&times;</button>
</div>
{% endfor %}
{% endif %}
@@ -166,10 +194,27 @@
</div>
<div class="auth-container">
<div class="auth-header">
<i class="fas fa-trophy"></i>
<h2>TryoutPro</h2>
<p>Team Tryout Management System</p>
<img src="{{ url_for('static', filename='images/UdeS_logo.png') }}" alt="UdeS Logo" class="auth-logo-img">
<h2>UdeS team manager</h2>
<p>{{ _('UdeS team manager') }}</p>
</div>
<div class="auth-language">
{% include "layouts/_language_switcher.html" %}
</div>
{# The same `content` block as the signed-in branch, rendered
here too — `self.content()` rather than a second
`{% block %}`, which Jinja refuses.
The error pages (400, 403, 404, 429, 500) all fill `content`,
and it existed only inside the `is_authenticated` branch: a
signed-out visitor hitting any of them got the logo, the
language switcher and no message whatsoever. The <title> still
said "404", which is most of why nobody noticed.
Only one branch of the `if` runs, so this never double-renders.
Sign-in pages fill `auth_content` instead and leave this
empty. #}
{{ self.content() }}
{% block auth_content %}{% endblock %}
</div>
</div>
@@ -178,4 +223,4 @@
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
</html>
+4 -3
View File
@@ -7,7 +7,7 @@
{# Page Header Macro - renders title and breadcrumb #}
{% macro page_header(title, breadcrumb) %}
{% block title %}{{ title }} - TryoutPro{% endblock %}
{% block title %}{{ title }} - UdeS team manager{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">{{ breadcrumb }}</span>{% endblock %}
{% endmacro %}
@@ -108,11 +108,12 @@
{# Modal Macro - renders a modal dialog #}
{% macro modal(id, title, content, footer_buttons=None) %}
<div id="{{ id }}" class="modal hidden">
<div class="modal-backdrop" onclick="hideModal('{{ id }}')"></div>
<div class="modal-backdrop" data-action="hide-modal" data-modal-id="{{ id }}"></div>
<div class="modal-content">
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="modal-close" onclick="hideModal('{{ id }}')">&times;</button>
<button class="modal-close" data-action="hide-modal" data-modal-id="{{ id }}"
aria-label="{{ _('Close') }}">&times;</button>
</div>
<div class="modal-body">
{{ content }}