Merge branch 'dev' of https://git.immortal.host/clubesportsudes/team-tryouts into dev
This commit is contained in:
@@ -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 %}
|
||||
@@ -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">« {{ _('Previous') }}</a>
|
||||
{% else %}
|
||||
<span class="btn btn-secondary btn-sm is-disabled" aria-disabled="true">« {{ _('Previous') }}</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="pagination-status">
|
||||
{{ _('Page %(page)s of %(pages)s', page=pagination.page, pages=pagination.pages) }}
|
||||
·
|
||||
{{ _('%(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') }} »</a>
|
||||
{% else %}
|
||||
<span class="btn btn-secondary btn-sm is-disabled" aria-disabled="true">{{ _('Next') }} »</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -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()">×</button>
|
||||
<button type="button" class="alert-close" data-action="dismiss-alert"
|
||||
aria-label="{{ _('Dismiss') }}">×</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()">×</button>
|
||||
<button type="button" class="alert-close" data-action="dismiss-alert"
|
||||
aria-label="{{ _('Dismiss') }}">×</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>
|
||||
|
||||
@@ -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 }}')">×</button>
|
||||
<button class="modal-close" data-action="hide-modal" data-modal-id="{{ id }}"
|
||||
aria-label="{{ _('Close') }}">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ content }}
|
||||
|
||||
Reference in New Issue
Block a user