Make the management app responsive on mobile

Replace the mobile nav's horizontal scroll strip (which wrapped into
an unreadable jumble because daisyUI's .menu sets flex-wrap: wrap and
.menu-horizontal never resets it) with a hamburger button that opens
a proper left-side drawer, mirrored in controlpanel for the same bug.
Move the theme toggle, "Management", and "Django admin" controls into
that drawer below `lg`, leaving only the account icon in the navbar,
so the club name has room to render in full instead of truncating to
initials.

Add a reusable `.table-cards` CSS pattern (app.css) that turns a list
table into a stack of labelled cards below `md`, and apply it to every
list/detail table in the management app -- members, families, teams,
memberships, events, groups, locations, opponents, positions, referee
levels/list, roles, sponsors, news, parent claims, team roster/staff,
and the shared family-members table. Tables revert to normal desktop
layout at `md` and up.

Member list gets extra passes: search submits icon-only below `sm` so
it fits next to the input, the Members/Guardians/Both tabs go
full-width, and last/first name merge into one column. Action-button
rows across every page stack full-width on mobile instead of wrapping
mid-button (shared fix in base.html). Home dashboard's upcoming-events
and news mini-tables become simple lists instead of overflowing
tables. Also: brand truncates instead of overflowing on a long club
name, a stray invalid xmlns attribute removed, a wrapping UUID badge
hidden below `sm` on team detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 09:55:08 +02:00
parent 5d3dbebd77
commit 9f4a0ea687
35 changed files with 674 additions and 699 deletions

View File

@@ -210,3 +210,59 @@
backface-visibility: hidden;
transform: translateZ(0);
}
/* Below `md`, a `.table-cards` table reads as a stack of cards instead of a
horizontally-scrolling grid: each row becomes a bordered block, each cell
becomes a label/value line pulling its label from `data-label` (set on the
<td> in the template -- there's no way to read the matching <th> text from
pure CSS). A <td> with no `data-label` renders as plain full-width content
instead of a label/value row -- for an image cell, a heading-style link, or
an actions row that already lays itself out. At `md` and up this reverts to
an ordinary table with nothing left over from the mobile styling. */
@media (width < 48rem) {
.table-cards, .table-cards :is(thead, tbody, tr, th, td) {
display: block;
width: 100%;
}
.table-cards thead {
display: none;
}
.table-cards tbody tr {
border: 1px solid var(--color-base-300);
border-radius: var(--radius-box);
padding: 0 0.75rem;
}
.table-cards tbody tr + tr {
margin-top: 0.75rem;
}
.table-cards td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--color-base-200);
}
.table-cards td:last-child {
border-bottom: none;
}
.table-cards td[data-label]:before {
content: attr(data-label);
flex-shrink: 0;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
opacity: 0.5;
}
.table-cards td:not([data-label]) {
justify-content: flex-start;
}
}

View File

@@ -5,6 +5,15 @@
{% block panel_title %}Control panel{% endblock panel_title %} · RosterChief
{% endblock title %}
{% block nav_toggle %}
<button class="btn btn-ghost btn-square lg:hidden" type="button" onclick="document.getElementById('mobile_nav_modal').showModal()" aria-label="Menu">
{% lucide "menu" size=20 %}
</button>
{% endblock nav_toggle %}
{% comment %} Kept in the navbar itself only at `lg`+, where there's no hamburger drawer to hold them instead -- see the comment on `nav_icons_class` in _base.html. {% endcomment %}
{% block nav_icons_class %}hidden items-center lg:flex{% endblock nav_icons_class %}
{% block menu %}
{% comment %}
Outside <main>, so it never scrolls with the content. Its own overflow-y-auto is for
@@ -18,6 +27,38 @@
{% endblock menu %}
{% block main %}
{# Below `lg` the sidebar is hidden and {% block nav_toggle %} above opens this instead. #}
<dialog id="mobile_nav_modal" class="modal modal-start lg:hidden">
<div class="modal-box h-full max-h-none w-72 max-w-[85vw] rounded-none p-0">
<div class="flex items-center justify-between border-b border-base-300 p-3">
<span class="font-roboto text-lg font-bold tracking-wider">Menu</span>
<form method="dialog">
<button class="btn btn-ghost btn-square btn-sm" aria-label="Close">{% lucide "x" size=18 %}</button>
</form>
</div>
<ul class="menu w-full gap-1 p-3">
<li>
<button type="button" data-theme-toggle>
<span data-theme-icon="light" class="hidden items-center gap-3">{% lucide "sun" size=16 %} Light theme</span>
<span data-theme-icon="dark" class="hidden items-center gap-3">{% lucide "moon" size=16 %} Dark theme</span>
<span data-theme-icon="auto" class="hidden items-center gap-3">{% lucide "sun-moon" size=16 %} Auto theme</span>
</button>
</li>
{% if has_management_access %}
<li><a href="{% url "management:home" %}">{% lucide "layout-dashboard" size=16 %} Management</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a href="{% url "admin:index" %}">{% lucide "shield-cog" size=16 %} Django admin</a></li>
{% endif %}
<li class="menu-title">Navigation</li>
{% include "controlpanel/_nav_items.html" %}
</ul>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{% if maintenance_on %}
<div class="alert alert-error mb-6">
{% lucide "wrench" size=20 %}
@@ -28,7 +69,8 @@
<a class="btn btn-sm gap-2 btn-error btn-soft" href="{% url 'controlpanel:features' %}">{% lucide "unlock" size=16 %} Reopen platform</a>
</div>
{% endif %}
<div class="mb-6 flex flex-wrap flex-row items-center justify-between gap-3">
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
<div class="flex flex-row items-center gap-3">
{% block logo %}{% endblock logo %}
<div class="flex flex-col gap-2 grow">
@@ -37,16 +79,12 @@
</h1>
<span class="text-sm text-base-content/50">{% block subheading %}{% endblock subheading %}</span>
</div>
</div>
<div class="flex gap-2">
<div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
{% block actions %}{% endblock actions %}
</div>
</div>
{# Below `lg` the sidebar is hidden, so the same links appear here rather than nowhere. #}
<ul class="menu menu-horizontal mb-6 w-full gap-1 overflow-x-auto rounded-box bg-base-100 lg:hidden">
{% include "controlpanel/_nav_items.html" %}
</ul>
{% block panel %}{% endblock panel %}
{% endblock main %}

View File

@@ -10,11 +10,10 @@ passes its own URL so admins land back on the member they were viewing; family_d
leaves it unset, since staying on the family page is already the right place there.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Last name" %}</th>
<th>{% trans "First name" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Type" %}</th>
<th></th>
@@ -23,10 +22,9 @@ leaves it unset, since staying on the family page is already the right place the
<tbody>
{% for person in group.all %}
<tr>
<td><a class="link link-hover" href="{% url 'management:member_detail' person.pk %}">{{ person.last_name }}</a></td>
<td>{{ person.first_name }}</td>
<td>{{ person.contact_email|default:"-" }}</td>
<td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' person.pk %}">{{ person.last_name }}, {{ person.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ person.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Type' %}">
{% if is_club_admin %}
<form method="post" action="{% url 'management:family_membership_role_update' group.family.pk person.pk %}">
{% csrf_token %}
@@ -43,7 +41,7 @@ leaves it unset, since staying on the family page is already the right place the
</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex justify-end gap-1">
<div class="flex flex-wrap justify-end gap-1">
{% if person.grant_login_form %}
<button class="btn btn-outline btn-sm" type="button" onclick="document.getElementById('grant_login_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Grant login' %}">
{% lucide "key-round" size=14 %} {% trans "Grant login" %}

View File

@@ -6,7 +6,7 @@
can_manage_referees, and an optional next_url to return to after a POST
(defaults to the event detail page when blank).
{% endcomment %}
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">
{% lucide "flag" size=18 %} {% trans "Referees" %}
<span class="badge badge-sm {% if referees_full %}badge-neutral{% else %}badge-outline{% endif %}">

View File

@@ -11,6 +11,15 @@
{% block section_title %}{% trans "Management" %}{% endblock section_title %}
{% endblock head_title %}
{% block nav_toggle %}
<button class="btn btn-ghost btn-square lg:hidden" type="button" onclick="document.getElementById('mobile_nav_modal').showModal()" aria-label="{% trans 'Menu' %}">
{% lucide "menu" size=20 %}
</button>
{% endblock nav_toggle %}
{% comment %} Kept in the navbar itself only at `lg`+, where there's no hamburger drawer to hold them instead -- see the comment on `nav_icons_class` in _base.html. {% endcomment %}
{% block nav_icons_class %}hidden items-center lg:flex{% endblock nav_icons_class %}
{% block menu %}
<aside class="hidden w-64 shrink-0 overflow-y-auto border-r border-base-300 bg-base-100 lg:block">
<ul class="menu w-full gap-1 p-3 mt-4">
@@ -20,7 +29,40 @@
{% endblock menu %}
{% block main %}
<div class="mb-6 flex flex-wrap flex-row items-center justify-between gap-3">
{# Below `lg` the sidebar is hidden and {% block nav_toggle %} above opens this instead. #}
<dialog id="mobile_nav_modal" class="modal modal-start lg:hidden">
<div class="modal-box h-full max-h-none w-72 max-w-[85vw] rounded-none p-0">
<div class="flex items-center justify-between border-b border-base-300 p-3">
<span class="font-roboto text-lg font-bold tracking-wider">{% trans "Menu" %}</span>
<form method="dialog">
<button class="btn btn-ghost btn-square btn-sm" aria-label="{% trans 'Close' %}">{% lucide "x" size=18 %}</button>
</form>
</div>
<ul class="menu w-full gap-1 p-3">
<li>
<button type="button" data-theme-toggle>
<span data-theme-icon="light" class="hidden items-center gap-3">{% lucide "sun" size=16 %} {% trans "Light theme" %}</span>
<span data-theme-icon="dark" class="hidden items-center gap-3">{% lucide "moon" size=16 %} {% trans "Dark theme" %}</span>
<span data-theme-icon="auto" class="hidden items-center gap-3">{% lucide "sun-moon" size=16 %} {% trans "Auto theme" %}</span>
</button>
</li>
{% if has_management_access %}
<li><a href="{% url "management:home" %}">{% lucide "layout-dashboard" size=16 %} {% trans "Management" %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a href="{% url "admin:index" %}">{% lucide "shield-cog" size=16 %} {% trans "Django admin" %}</a></li>
{% endif %}
<li class="menu-title">{% trans "Navigation" %}</li>
{% include "management/_nav_items.html" %}
</ul>
</div>
<form method="dialog" class="modal-backdrop">
<button>{% trans "close" %}</button>
</form>
</dialog>
{# flex-col below `sm`: title over a full-width, stacked column of action buttons reads far better on a phone than the same row wrapping mid-button. #}
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
<div class="flex flex-col gap-2 grow">
<h1 class="text-3xl font-bold">
{% block heading %}{% trans "Management" %}{% endblock heading %}
@@ -28,16 +70,11 @@
<span class="text-sm text-base-content/50">{% block subheading %}{% endblock subheading %}</span>
</div>
<div class="flex gap-2">
<div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
{% block actions %}{% endblock actions %}
</div>
</div>
{# Below `lg` the sidebar is hidden, so the same links appear here rather than nowhere. #}
<ul class="menu menu-horizontal mb-6 w-full gap-1 overflow-x-auto rounded-box bg-base-100 lg:hidden">
{% include "management/_nav_items.html" %}
</ul>
{% comment %}
A final billing notice follows the admin onto every management page -- but only at
error level (inside 7 days of archiving, or already past it). Shown from the moment

View File

@@ -81,7 +81,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "user-check" size=18 %} {% trans "RSVPs" %}</h2>
{% if has_attendance_rows %}
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('rsvp_modal').showModal()">
@@ -107,7 +107,7 @@
{% if event.kind == "game" %}
<div class="mb-6 card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">
{% lucide "trophy" size=18 %} {% trans "Game" %}
{% if event.is_live %}<span class="badge badge-error gap-1 animate-pulse">{% lucide "circle" size=10 %} {% trans "Live" %}</span>{% endif %}

View File

@@ -38,7 +38,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Title" %}</th>
@@ -52,14 +52,14 @@
<tbody>
{% for event in events %}
<tr>
<td class="flex flex-row items-center gap-2">
<td class="flex flex-row items-center gap-2 font-semibold">
<a class="link link-hover" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
{% if event.series_id %}<span class="tooltip" data-tip="{% trans 'Part of a series' %}">{% lucide "repeat" size=12 %}</span>{% endif %}
</td>
<td>{{ event.get_kind_display }}</td>
<td>{{ event.start|date:"j M Y H:i" }}</td>
<td>{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</td>
<td>{{ event.location.name|default:"—" }}</td>
<td data-label="{% trans 'Kind' %}">{{ event.get_kind_display }}</td>
<td data-label="{% trans 'When' %}">{{ event.start|date:"j M Y H:i" }}</td>
<td data-label="{% trans 'Teams' %}">{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</td>
<td data-label="{% trans 'Location' %}">{{ event.location.name|default:"—" }}</td>
<td class="text-right">
{% if event.can_manage %}
<div class="flex justify-end gap-1">

View File

@@ -67,7 +67,7 @@
<div class="card-body">
<h2 class="card-title text-base">{% lucide "calendar" size=18 %} {% trans "Occurrences" %}</h2>
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "When" %}</th>
@@ -77,8 +77,8 @@
<tbody>
{% for occurrence in occurrences %}
<tr>
<td><a class="link link-hover" href="{% url 'management:event_detail' occurrence.pk %}">{{ occurrence.start|date:"j M Y H:i" }}</a></td>
<td>
<td class="font-semibold"><a class="link link-hover" href="{% url 'management:event_detail' occurrence.pk %}">{{ occurrence.start|date:"j M Y H:i" }}</a></td>
<td data-label="{% trans 'Status' %}">
{% if occurrence.cancelled %}
<span class="badge badge-outline gap-1">{% lucide "ban" size=12 %} {% trans "Cancelled" %}</span>
{% elif occurrence.detached %}

View File

@@ -25,7 +25,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Family" %}</th>
@@ -38,14 +38,14 @@
{% for family in families %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:family_detail' family.pk %}">{{ family }}</a></td>
<td>
<td data-label="{% trans 'Parents / guardians' %}">
{% for guardian in family.guardians_display %}
<a class="link link-hover" href="{% url 'management:member_detail' guardian.pk %}">{{ guardian }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="opacity-40">-</span>
{% endfor %}
</td>
<td>
<td data-label="{% trans 'Children' %}">
{% for child in family.children_display %}
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}

View File

@@ -18,10 +18,12 @@
{% endfor %}
{% comment %}
Deliberately not wrapped in the usual overflow-x-auto: that makes a
scroll container, which clips the member picker's absolutely
positioned dropdown for every row but the first couple.
overflow-x-auto is safe here: the member picker's dropdown
(searchable-select.js) is position: fixed, computed from the input's
own screen position rather than document flow, so a scrolling
ancestor around the table no longer clips it.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
@@ -35,6 +37,7 @@
{% endfor %}
</tbody>
</table>
</div>
<template id="bulk-add-row-template">
{% include "management/_group_bulk_add_row.html" with form=formset.empty_form %}

View File

@@ -10,12 +10,12 @@
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Members" %}</h2>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:group_bulk_add' group.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
</div>
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
@@ -25,7 +25,7 @@
<tbody>
{% for membership in memberships %}
<tr>
<td><a class="link link-hover" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member }}</a></td>
<td class="font-semibold"><a class="link link-hover" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member }}</a></td>
<td class="text-right">
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"remove_member_modal" }}').showModal()">{% lucide "user-minus" size=14 %} {% trans "Remove" %}</button>
</td>

View File

@@ -12,7 +12,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
@@ -23,8 +23,8 @@
<tbody>
{% for group in groups %}
<tr>
<td><a class="link link-hover" href="{% url 'management:group_detail' group.pk %}">{{ group.name }}</a></td>
<td>{{ group.member_count }}</td>
<td><a class="link link-hover font-semibold" href="{% url 'management:group_detail' group.pk %}">{{ group.name }}</a></td>
<td data-label="{% trans 'Members' %}">{{ group.member_count }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:group_detail' group.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>

View File

@@ -96,53 +96,45 @@
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "calendar" size=18 %} {% trans "Upcoming events" %}</h2>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:event_list' %}">{% lucide "arrow-right" size=14 %} {% trans "View all" %}</a>
</div>
<div class="overflow-x-auto">
<table class="table">
<tbody>
<ul class="divide-y divide-base-200">
{% for event in upcoming_events %}
<tr>
<td class="whitespace-nowrap">{{ event.start|date:"D j M, H:i" }}</td>
<td>{{ event.title }}</td>
<td>{{ event.get_kind_display }}</td>
<td>{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</td>
</tr>
{% empty %}
<tr>
<td class="text-center opacity-60">{% trans "Nothing scheduled." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div class="min-w-0">
<div class="truncate font-semibold">{{ event.title }}</div>
<div class="truncate text-xs opacity-60">
{{ event.get_kind_display }}
{% for team in event.teams.all %}&middot; {{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
</div>
</div>
<div class="shrink-0 whitespace-nowrap text-sm opacity-70">{{ event.start|date:"D j M, H:i" }}</div>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "Nothing scheduled." %}</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "newspaper" size=18 %} {% trans "News" %}</h2>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:news_list' %}">{% lucide "arrow-right" size=14 %} {% trans "View all" %}</a>
</div>
<div class="overflow-x-auto">
<table class="table">
<tbody>
<ul class="divide-y divide-base-200">
{% for news_item in published_news %}
<tr>
<td class="whitespace-nowrap">{{ news_item.published_at|date:"D j M" }}</td>
<td><a class="link link-hover" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a></td>
</tr>
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<a class="link link-hover truncate font-semibold" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a>
<span class="shrink-0 whitespace-nowrap text-sm opacity-70">{{ news_item.published_at|date:"D j M" }}</span>
</li>
{% empty %}
<tr>
<td class="text-center opacity-60">{% trans "Nothing published yet." %}</td>
</tr>
<li class="py-2 text-center text-sm opacity-60">{% trans "Nothing published yet." %}</li>
{% endfor %}
</tbody>
</table>
</div>
</ul>
</div>
</div>
</div>

View File

@@ -11,7 +11,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
@@ -24,13 +24,13 @@
<tbody>
{% for location in locations %}
<tr>
<td>
<td class="font-semibold">
{{ location.name }}
{% if location.is_home %}<span class="badge badge-sm badge-primary ml-1">{% trans "Home" %}</span>{% endif %}
</td>
<td>{{ location.address }}</td>
<td>{{ location.city }}</td>
<td>{{ location.country }}</td>
<td data-label="{% trans 'Address' %}">{{ location.address }}</td>
<td data-label="{% trans 'City' %}">{{ location.city }}</td>
<td data-label="{% trans 'Country' %}">{{ location.country }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:location_update' location.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>

View File

@@ -119,7 +119,7 @@
<div class="divider"></div>
<h3 class="text-sm font-semibold opacity-70">{% trans "Season history" %}</h3>
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Season" %}</th>
@@ -131,10 +131,10 @@
<tbody>
{% for membership in membership_history %}
<tr>
<td>{{ membership.season.start_date|date:"Y" }} - {{ membership.season.end_date|date:"Y" }}</td>
<td>{{ membership.get_status_display }}</td>
<td>{{ membership.get_fee_status_display }}</td>
<td>{{ membership.license|default:"-" }}</td>
<td class="font-semibold">{{ membership.season.start_date|date:"Y" }} - {{ membership.season.end_date|date:"Y" }}</td>
<td data-label="{% trans 'Status' %}">{{ membership.get_status_display }}</td>
<td data-label="{% trans 'Fee status' %}">{{ membership.get_fee_status_display }}</td>
<td data-label="{% trans 'License' %}">{{ membership.license|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
@@ -148,7 +148,7 @@
{% if referee_profile or is_club_admin %}
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "flag" size=18 %} {% trans "Referee eligibility" %}</h2>
{% if is_club_admin %}
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('referee_eligibility_modal').showModal()">{% lucide "pencil" size=14 %} {% trans "Edit" %}</button>
@@ -214,13 +214,13 @@
{% for family_group in family_groups %}
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">
{% lucide "users" size=18 %}
<a class="link link-hover" href="{% url 'management:family_detail' family_group.family.pk %}">{{ family_group.family }}</a>
</h2>
{% if is_club_admin %}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_parent_modal" }}').showModal()">{% lucide "user-plus" size=14 %} {{ add_parent_label }}</button>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_child_modal" }}').showModal()">{% lucide "baby" size=14 %} {{ add_child_label }}</button>
<button class="btn btn-outline btn-error btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"detach_family_modal" }}').showModal()">{% lucide "user-x" size=14 %} {{ remove_from_family_label }}</button>
@@ -245,7 +245,7 @@
{% if is_club_admin %}
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "users" size=18 %} {% trans "Family" %}</h2>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('attach_family_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add to family" %}</button>
</div>

View File

@@ -4,24 +4,25 @@
{% block heading %}{% trans "Members" %}{% endblock heading %}
{% block actions %}
<a class="btn btn-outline btn-info gap-2" href="{% url 'management:member_import_template' %}" xmlns:management="http://www.w3.org/1999/xhtml">{% lucide "download" size=16 %} {% trans "Download upload template" %}</a>
<a class="btn btn-sm btn-outline btn-info gap-2" href="{% url 'management:member_import_template' %}">{% lucide "download" size=14 %} {% trans "Download upload template" %}</a>
{% if is_club_admin %}
<a class="btn btn-outline btn-info gap-2" href="{% url 'management:member_import' %}">{% lucide "upload" size=16 %} {% trans "Mass upload" %}</a>
<a class="btn btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=16 %} {% trans "Add family" %}</a>
<a class="btn btn-outline gap-2" href="{% url 'management:member_create' %}">{% lucide "user-plus" size=16 %} {% trans "Add member" %}</a>
<a class="btn btn-sm btn-outline btn-info gap-2" href="{% url 'management:member_import' %}">{% lucide "upload" size=14 %} {% trans "Mass upload" %}</a>
<a class="btn btn-sm btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=14 %} {% trans "Add family" %}</a>
<a class="btn btn-sm btn-outline gap-2" href="{% url 'management:member_create' %}">{% lucide "user-plus" size=14 %} {% trans "Add member" %}</a>
{% endif %}
{% endblock actions %}
{% block panel %}
<form method="get" class="mb-2 flex flex-wrap items-center gap-2">
<label class="input">
<form method="get" class="mb-2 flex items-center gap-2">
<label class="input grow sm:grow-0">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full max-w-xs">
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full sm:max-w-xs">
</label>
{% if selected_kind != "member" %}<input type="hidden" name="kind" value="{{ selected_kind }}">{% endif %}
<button class="btn btn-outline gap-2" type="submit">{% lucide "search" size=16 %} {% trans "Search" %}</button>
{# Icon-only below `sm`: label text next to a growing input is what forced this row to wrap on a phone. #}
<button class="btn btn-outline gap-2" type="submit" aria-label="{% trans 'Search' %}">{% lucide "search" size=16 %}<span class="hidden sm:inline">{% trans "Search" %}</span></button>
{% if search %}
<a class="btn gap-2" href="{% url "management:member_list" %}">{% lucide "x" size=16 %} {% trans "Clear filter" %}</a>
<a class="btn gap-2" href="{% url "management:member_list" %}" aria-label="{% trans 'Clear filter' %}">{% lucide "x" size=16 %}<span class="hidden sm:inline">{% trans "Clear filter" %}</span></a>
{% endif %}
</form>
@@ -33,23 +34,23 @@
with nothing on the page explaining why. ?kind= makes the exclusion an
explicit, reversible choice instead.
{% endcomment %}
<div class="mb-4 flex flex-wrap items-center gap-2">
<div class="join">
<a href="{% querystring kind=None page=None %}" class="btn btn-sm join-item {% if selected_kind == "member" %}btn-primary{% endif %}">{% trans "Members" %}</a>
<a href="{% querystring kind="guardian" page=None %}" class="btn btn-sm join-item {% if selected_kind == "guardian" %}btn-primary{% endif %}">{% trans "Guardians" %}</a>
<a href="{% querystring kind="both" page=None %}" class="btn btn-sm join-item {% if selected_kind == "both" %}btn-primary{% endif %}">{% trans "Both" %}</a>
<div class="mb-4 flex flex-col gap-2">
<div class="join w-full">
<a href="{% querystring kind=None page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "member" %}btn-primary{% endif %}">{% trans "Members" %}</a>
<a href="{% querystring kind="guardian" page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "guardian" %}btn-primary{% endif %}">{% trans "Guardians" %}</a>
<a href="{% querystring kind="both" page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "both" %}btn-primary{% endif %}">{% trans "Both" %}</a>
</div>
<span class="text-xs opacity-60">{% trans "Guardians are parents linked only through a child -- no fee, not counted as a member." %}</span>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
{# table-cards: below `md` each row reads as a card instead of forcing a horizontal scroll -- see the .table-cards rule in app.css. #}
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Last name" %}</th>
<th>{% trans "First name" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
@@ -59,10 +60,9 @@
<tbody>
{% for member in members %}
<tr>
<td><a class="link link-hover" href="{% url 'management:member_detail' member.pk %}">{{ member.last_name }}</a></td>
<td>{{ member.first_name }}</td>
<td>{{ member.contact_email|default:"-" }}</td>
<td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' member.pk %}">{{ member.last_name }}, {{ member.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ member.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Family' %}">
{% if member.family_memberships_display %}
<div class="flex flex-col gap-1">
{% for fm in member.family_memberships_display %}
@@ -75,7 +75,7 @@
<span class="opacity-40">-</span>
{% endif %}
</td>
<td>
<td data-label="{% trans 'Status' %}">
{% if member.current_membership %}
<span class="badge badge-sm
{% if member.current_membership.status == "active" %}badge-success
@@ -90,7 +90,7 @@
</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex justify-end gap-1">
<div class="flex flex-wrap justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:member_detail' member.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ member.pk|dom_id:"member_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
@@ -99,7 +99,7 @@
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center opacity-60">{% trans "No members yet." %}</td>
<td colspan="5" class="text-center opacity-60">{% trans "No members yet." %}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -59,19 +59,19 @@
{% endif %}
<form method="get" class="mb-4">
<div class="flex flex-row items-center gap-2">
<label class="input">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:flex lg:flex-row lg:flex-wrap lg:items-center">
<label class="input w-full lg:w-auto">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered">
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full">
</label>
<select name="season" class="select select-bordered">
<select name="season" class="select select-bordered w-full lg:w-auto">
{% for season in seasons %}
<option value="{{ season.pk }}" {% if season.pk == selected_season.pk %}selected{% endif %}>{% trans "Season" %} {{ season.start_date|date:"Y" }} - {{ season.end_date|date:"Y" }}</option>
{% endfor %}
</select>
<select name="fee_status" class="select select-bordered">
<select name="fee_status" class="select select-bordered w-full lg:w-auto">
<option value="not_paid" {% if selected_fee_status == "not_paid" %}selected{% endif %}>{% trans "Not paid (unpaid or partially)" %}</option>
{% for value, label in fee_status_choices %}
<option value="{{ value }}" {% if value == selected_fee_status %}selected{% endif %}>{{ label|capfirst }}</option>
@@ -79,22 +79,24 @@
<option value="all" {% if selected_fee_status == "all" %}selected{% endif %}>{% trans "All" %}</option>
</select>
<select name="status" class="select select-bordered">
<select name="status" class="select select-bordered w-full lg:w-auto">
<option value="all" {% if selected_status == "all" %}selected{% endif %}>{% trans "All statuses" %}</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if value == selected_status %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
<select name="team" class="select select-bordered">
<select name="team" class="select select-bordered w-full lg:w-auto">
<option value="" {% if not selected_team %}selected{% endif %}>{% trans "All teams" %}</option>
{% for team in teams %}
<option value="{{ team.pk }}" {% if team.pk|stringformat:"s" == selected_team %}selected{% endif %}>{{ team }}</option>
{% endfor %}
</select>
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
<a class="btn gap-2" href="{% url 'management:membership_list' %}">{% lucide "x" size=16 %} {% trans "Reset" %}</a>
<div class="col-span-full flex gap-2 lg:col-span-1 lg:contents">
<button class="btn btn-outline grow gap-2 lg:grow-0" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
<a class="btn grow gap-2 lg:grow-0" href="{% url 'management:membership_list' %}">{% lucide "x" size=16 %} {% trans "Reset" %}</a>
</div>
</div>
</form>
@@ -104,20 +106,20 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between mb-2">
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<span class="text-sm opacity-70 font-semibold">
{% blocktrans count counter=memberships|length %}{{ counter }} membership{% plural %}{{ counter }} memberships{% endblocktrans %}
</span>
<button class="btn btn-success btn-sm gap-2" type="submit">{% lucide "circle-check" size=14 %} {% trans "Mark selected as paid" %}</button>
</div>
{# table-cards: this table is dense enough (10+ columns) that horizontal scroll alone stops being usable below `md` -- see the .table-cards rule in app.css. #}
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th><input type="checkbox" class="checkbox" id="select-all-memberships"></th>
<th>{% trans "Last name" %}</th>
<th>{% trans "First name" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
@@ -132,10 +134,9 @@
{% for membership in memberships %}
<tr>
<td><input type="checkbox" class="checkbox membership-row-checkbox" name="membership_ids" value="{{ membership.pk }}"></td>
<td><a class="link link-hover" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member.last_name }}</a></td>
<td>{{ membership.member.first_name }}</td>
<td>{{ membership.member.contact_email|default:"-" }}</td>
<td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member.last_name }}, {{ membership.member.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ membership.member.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Family' %}">
{% if membership.member.family_memberships_display %}
{% for fm in membership.member.family_memberships_display %}
{{ fm.family }} {% if not forloop.last %},{% endif %}
@@ -144,7 +145,7 @@
<span class="opacity-40">-</span>
{% endif %}
</td>
<td>
<td data-label="{% trans 'Status' %}">
<span class="badge badge-sm
{% if membership.status == "active" %}badge-success
{% elif membership.status == "pending" %}badge-warning
@@ -153,7 +154,7 @@
{{ membership.get_status_display }}
</span>
</td>
<td>
<td data-label="{% trans 'Fee status' %}">
<span class="badge badge-sm
{% if membership.fee_status == "paid" %}badge-success
{% elif membership.fee_status == "partially_paid" %}badge-warning
@@ -162,17 +163,17 @@
{{ membership.get_fee_status_display }}
</span>
</td>
<td class="tabular-nums">&euro; {{ membership.fee_amount }}</td>
<td class="tabular-nums">
<td data-label="{% trans 'Owed' %}" class="tabular-nums">&euro; {{ membership.fee_amount }}</td>
<td data-label="{% trans 'Paid' %}" class="tabular-nums">
&euro; {{ membership.amount_paid }}
{% if membership.record_payment_form and membership.fee_amount %}
<div class="text-xs opacity-60">{% blocktrans with remaining=membership.remaining_balance_display %}{{ remaining }} left{% endblocktrans %}</div>
{% endif %}
</td>
<td>{{ membership.license|default:"-" }}</td>
<td data-label="{% trans 'License' %}">{{ membership.license|default:"-" }}</td>
<td class="text-right">
{% if membership.record_payment_form %}
<div class="flex justify-end gap-1">
<div class="flex flex-wrap justify-end gap-1">
<button class="btn btn-outline btn-sm" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"record_payment_modal" }}').showModal()">
{% lucide "receipt" size=12 %} {% trans "Record payment" %}
</button>
@@ -185,7 +186,7 @@
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center opacity-60">{% trans "Nobody matches these filters." %}</td>
<td colspan="10" class="text-center opacity-60">{% trans "Nobody matches these filters." %}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -53,7 +53,7 @@
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Photos" %}</h2>
{% if can_edit %}
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_photos_modal').showModal()">{% lucide "image-plus" size=14 %} {% trans "Add photos" %}</button>

View File

@@ -13,7 +13,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Title" %}</th>
@@ -26,16 +26,16 @@
<tbody>
{% for news_item in news_items %}
<tr>
<td><a class="link link-hover" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a></td>
<td>
<td><a class="link link-hover font-semibold" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a></td>
<td data-label="{% trans 'Teams' %}">
{% for team in news_item.teams.all %}
<span class="badge badge-neutral badge-sm">{{ team.short_name }}</span>
{% empty %}
<span class="opacity-60">{% trans "Club-wide" %}</span>
{% endfor %}
</td>
<td>{{ news_item.get_visibility_display }}</td>
<td>
<td data-label="{% trans 'Visibility' %}">{{ news_item.get_visibility_display }}</td>
<td data-label="{% trans 'Status' %}">
{% if news_item.status == "draft" %}
<span class="badge badge-neutral badge-sm">{% trans "Draft" %}</span>
{% elif news_item.is_scheduled %}

View File

@@ -11,7 +11,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th></th>
@@ -27,7 +27,7 @@
<img src="{{ opponent.logo.url }}" alt="" class="size-8 rounded object-contain">
{% endif %}
</td>
<td>{{ opponent.name }}</td>
<td class="font-semibold">{{ opponent.name }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:opponent_update' opponent.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>

View File

@@ -92,7 +92,7 @@
<div class="card-body">
<h2 class="card-title text-base">{% lucide "history" size=18 %} {% trans "Already dealt with" %}</h2>
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Parent" %}</th>
@@ -104,9 +104,9 @@
<tbody>
{% for claim in reviewed %}
<tr>
<td>{{ claim.parent_name }}<div class="text-xs opacity-60">{{ claim.parent_email }}</div></td>
<td>{{ claim.claimed_child_name }}</td>
<td>
<td class="font-semibold">{{ claim.parent_name }}<div class="text-xs font-normal opacity-60">{{ claim.parent_email }}</div></td>
<td data-label="{% trans 'Claimed' %}">{{ claim.claimed_child_name }}</td>
<td data-label="{% trans 'Outcome' %}">
{% if claim.status == "approved" %}
<span class="badge badge-success badge-sm">{% trans "Approved" %}</span>
{% if claim.child %}<div class="text-xs opacity-60">{{ claim.child }}</div>{% endif %}
@@ -115,7 +115,7 @@
{% if claim.note %}<div class="text-xs opacity-60">{{ claim.note }}</div>{% endif %}
{% endif %}
</td>
<td>{{ claim.reviewed_by|default:"—" }}</td>
<td data-label="{% trans 'Reviewed by' %}">{{ claim.reviewed_by|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -13,7 +13,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Ordering" %}</th>
@@ -27,15 +27,15 @@
<tbody>
{% for position in positions %}
<tr>
<td>{{ position.ordering }}</td>
<td>{{ position.name }}</td>
<td>{{ position.short_name }}</td>
<td>
<td data-label="{% trans 'Ordering' %}">{{ position.ordering }}</td>
<td class="font-semibold">{{ position.name }}</td>
<td data-label="{% trans 'Short name' %}">{{ position.short_name }}</td>
<td data-label="{% trans 'Staff position' %}">
<span class="badge badge-sm {% if position.staff_position %}badge-success{% else %}badge-neutral{% endif %}">
{% if position.staff_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>
</td>
<td>
<td data-label="{% trans 'Management position' %}">
<span class="badge badge-sm {% if position.management_position %}badge-success{% else %}badge-neutral{% endif %}">
{% if position.management_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>

View File

@@ -14,7 +14,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Ordering" %}</th>
@@ -26,9 +26,9 @@
<tbody>
{% for level in levels %}
<tr>
<td>{{ level.ordering }}</td>
<td>{{ level.name }}</td>
<td>
<td data-label="{% trans 'Ordering' %}">{{ level.ordering }}</td>
<td class="font-semibold">{{ level.name }}</td>
<td data-label="{% trans 'Qualifies for' %}">
{% for team in level.teams.all %}
<span class="badge badge-outline badge-sm">{{ team.name }}</span>
{% empty %}

View File

@@ -8,7 +8,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Referee" %}</th>
@@ -21,9 +21,9 @@
<tbody>
{% for referee in referees %}
<tr>
<td><a class="link link-hover" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a></td>
<td>{{ referee.referee_profile.level|default:"—" }}</td>
<td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a></td>
<td data-label="{% trans 'Level' %}">{{ referee.referee_profile.level|default:"—" }}</td>
<td data-label="{% trans 'Can referee for' %}">
{% if referee.referee_profile.is_eligible %}
{% for team in referee.referee_profile.eligible_teams %}
<span class="badge badge-outline badge-sm">{{ team.name }}</span>
@@ -34,8 +34,8 @@
<span class="opacity-40"></span>
{% endif %}
</td>
<td>{{ referee.referee_profile.valid_until|default:"—" }}</td>
<td>
<td data-label="{% trans 'Valid until' %}">{{ referee.referee_profile.valid_until|default:"—" }}</td>
<td data-label="{% trans 'Status' %}">
{% if referee.referee_profile.is_eligible %}
<span class="badge badge-success badge-sm">{% trans "Valid" %}</span>
{% elif not referee.referee_profile.level %}

View File

@@ -59,7 +59,7 @@
<span class="opacity-60">{% trans "No referees assigned yet." %}</span>
{% endfor %}
</div>
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="badge badge-sm {% if not game.referee_rows %}badge-error{% elif not game.referees_full %}badge-warning{% else %}badge-success{% endif %}">
{{ game.referee_rows|length }} / {{ game.max_referees }}
</span>

View File

@@ -24,7 +24,7 @@
<h2 class="card-title text-base">{{ role_label }}</h2>
{% if description %}<p class="text-sm opacity-70">{{ description }}</p>{% endif %}
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
@@ -34,7 +34,7 @@
<tbody>
{% for role in roles_for_section %}
<tr>
<td>{{ role.member }}</td>
<td class="font-semibold">{{ role.member }}</td>
<td class="text-right">
<form method="post" action="{% url 'management:role_revoke' role.pk %}">
{% csrf_token %}

View File

@@ -11,7 +11,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th></th>
@@ -30,16 +30,16 @@
<img src="{{ sponsor.logo.url }}" alt="" class="size-8 rounded object-contain">
{% endif %}
</td>
<td>{{ sponsor.name }}</td>
<td>
<td class="font-semibold">{{ sponsor.name }}</td>
<td data-label="{% trans 'URL' %}">
{% if sponsor.url %}
<a class="link link-hover" href="{{ sponsor.url }}" target="_blank" rel="noopener noreferrer">{{ sponsor.url }}</a>
{% else %}
<span class="opacity-50">&mdash;</span>
{% endif %}
</td>
<td>{{ sponsor.start_date|date:"Y-m-d" }}</td>
<td>{{ sponsor.end_date|date:"Y-m-d"|default:"—" }}</td>
<td data-label="{% trans 'Start date' %}">{{ sponsor.start_date|date:"Y-m-d" }}</td>
<td data-label="{% trans 'End date' %}">{{ sponsor.end_date|date:"Y-m-d"|default:"—" }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:sponsor_update' sponsor.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>

View File

@@ -22,10 +22,12 @@
{% endfor %}
{% comment %}
Deliberately not wrapped in the usual overflow-x-auto: that makes a
scroll container, which clips the member picker's absolutely
positioned dropdown for every row but the first couple.
overflow-x-auto is safe here: the member picker's dropdown
(searchable-select.js) is position: fixed, computed from the input's
own screen position rather than document flow, so a scrolling
ancestor around the table no longer clips it.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
@@ -43,6 +45,7 @@
{% endfor %}
</tbody>
</table>
</div>
<template id="bulk-add-row-template">
{% include "management/_team_bulk_add_row.html" with form=formset.empty_form %}

View File

@@ -3,10 +3,11 @@
{% block heading %}{{ team.name }}{% endblock heading %}
{% block subheading %}
<span class="flex flex-row gap-2 items-center">
{# The raw id is a reference for support/debugging, not something a coach glancing at their phone needs -- hidden below `sm` rather than left to wrap awkwardly inside the pill. #}
<span class="flex flex-row items-center gap-2">
<span>{{ team.short_name }}</span>
<span>&middot;</span>
<span class="badge-sm badge badge-outline">{% lucide "bookmark" size=14 %} {{ team.pk }}</span>
<span class="hidden sm:inline">&middot;</span>
<span class="badge badge-outline badge-sm hidden sm:inline-flex">{% lucide "bookmark" size=14 %} {{ team.pk }}</span>
</span>
{% endblock subheading %}
@@ -18,7 +19,7 @@
{% block panel %}
<form method="get" class="mb-4">
<div class="flex flex-row items-center gap-2">
<div class="flex flex-row flex-wrap items-center gap-2">
<select name="season" class="select select-bordered">
{% for season in seasons %}
<option value="{{ season.pk }}" {% if season.pk == selected_season.pk %}selected{% endif %}>{% trans "Season" %} {{ season.start_date|date:"Y" }} - {{ season.end_date|date:"Y" }}</option>
@@ -123,40 +124,36 @@
<div class="card-body">
<h2 class="card-title text-base">{% lucide "user-x" size=18 %} {% trans "No-shows" %}</h2>
<!-- <p class="text-sm opacity-70">{% trans "Said they'd attend, but were checked in as absent." %}</p> -->
<div class="overflow-x-auto">
<table class="table">
<tbody>
<ul class="divide-y divide-base-200">
{% for entry in no_shows %}
<tr>
<td>{{ entry.member }}</td>
<td class="opacity-70">{{ entry.event.title }}</td>
<td class="whitespace-nowrap text-right opacity-60">{{ entry.event.start|date:"j M" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center opacity-60">{% trans "None recorded." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div class="min-w-0">
<span class="font-semibold">{{ entry.member }}</span>
<span class="text-sm opacity-70">&middot; {{ entry.event.title }}</span>
</div>
<div class="shrink-0 whitespace-nowrap text-sm opacity-60">{{ entry.event.start|date:"j M" }}</div>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "None recorded." %}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Roster" %}</h2>
{% if can_manage %}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:team_bulk_add' team.pk selected_season.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_player_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add player" %}</button>
</div>
{% endif %}
</div>
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
@@ -169,16 +166,16 @@
<tbody>
{% for membership in roster %}
<tr>
<td>{{ membership.member }}</td>
<td>{{ membership.position }}</td>
<td>{{ membership.jersey_number|default:"—" }}</td>
<td class="font-semibold">{{ membership.member }}</td>
<td data-label="{% trans 'Position' %}">{{ membership.position }}</td>
<td data-label="{% trans 'Jersey #' %}">{{ membership.jersey_number|default:"—" }}</td>
<td>
{% if membership.is_captain %}<span class="badge badge-neutral badge-sm">{% trans "Captain" %}</span>{% endif %}
{% if membership.is_alternate_captain %}<span class="badge badge-neutral badge-sm">{% trans "Alternate captain" %}</span>{% endif %}
</td>
<td class="text-right">
{% if can_manage %}
<div class="flex justify-end gap-1">
<div class="flex flex-wrap justify-end gap-1">
<button class="btn btn-outline btn-sm" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"edit_player_modal" }}').showModal()" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</button>
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"remove_player_modal" }}').showModal()" aria-label="{% trans 'Remove' %}">{% lucide "user-minus" size=14 %} {% trans "Remove" %}</button>
</div>
@@ -198,10 +195,10 @@
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Staff" %}</h2>
{% if can_manage %}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:team_bulk_add' team.pk selected_season.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_staff_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Assign staff" %}</button>
</div>
@@ -263,10 +260,10 @@
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Team photo" %}</h2>
{% if can_manage %}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{% trans "Upload photo" as upload_photo_label %}
{% trans "Replace photo" as replace_photo_label %}
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('team_photo_modal').showModal()">

View File

@@ -13,7 +13,7 @@
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
@@ -26,10 +26,10 @@
<tbody>
{% for team in teams %}
<tr>
<td><a class="link link-hover" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
<td>{{ team.short_name }}</td>
<td>{{ team.player_count }}</td>
<td>{{ team.staff_count }}</td>
<td><a class="link link-hover font-semibold" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
<td data-label="{% trans 'Short name' %}">{{ team.short_name }}</td>
<td data-label="{% trans 'Players' %}">{{ team.player_count }}</td>
<td data-label="{% trans 'Staff' %}">{{ team.staff_count }}</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex justify-end gap-1">

View File

@@ -31,14 +31,13 @@
--text-3xl--line-height: calc(2.25 / 1.875);
--text-4xl: 2.25rem;
--text-4xl--line-height: calc(2.5 / 2.25);
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--radius-lg: 0.5rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -776,26 +775,6 @@
}
}
}
.collapse-plus {
@layer daisyui.l1.l2 {
> .collapse-title:after {
position: absolute;
display: block;
height: 0.5rem;
width: 0.5rem;
@media (prefers-reduced-motion: no-preference) {
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
top: 0.9rem;
inset-inline-end: 1.4rem;
--tw-content: "+";
content: var(--tw-content);
pointer-events: none;
}
}
}
.dropdown {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -1321,21 +1300,6 @@
}
}
}
.collapse-open {
@layer daisyui.l1.l2 {
grid-template-rows: max-content 1fr;
> .collapse-content {
--overflow-delay: 0.2s;
overflow: revert-layer;
content-visibility: visible;
min-height: fit-content;
padding-bottom: 1rem;
@supports not (content-visibility: visible) {
visibility: visible;
}
}
}
}
.collapse {
visibility: collapse;
}
@@ -1404,27 +1368,6 @@
}
}
}
.toast {
@layer daisyui.l1.l2.l3 {
position: fixed;
inset-inline-start: auto;
inset-inline-end: calc(0.25rem * 4);
top: auto;
bottom: calc(0.25rem * 4);
display: flex;
flex-direction: column;
gap: calc(0.25rem * 2);
background-color: transparent;
translate: var(--toast-x, 0) var(--toast-y, 0);
width: max-content;
max-width: calc(100vw - 2rem);
& > * {
@media (prefers-reduced-motion: no-preference) {
animation: toast 0.25s ease-out;
}
}
}
}
.toggle {
@layer daisyui.l1.l2.l3 {
border: var(--border) solid currentColor;
@@ -1877,51 +1820,6 @@
}
}
}
.aura {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-block;
--aura-padding: 0.125rem;
padding: var(--aura-padding);
border-radius: calc(var(--aura-padding) + var(--aura-radius, var(--radius-box)));
animation: aura var(--tw-duration, 6s) linear infinite;
background-image: conic-gradient(from var(--aura-angle), transparent 225deg, currentColor);
&:has( > .card, > .alert) {
--aura-radius: var(--radius-box);
}
&:has( > .btn, > .input, > .select) {
--aura-radius: var(--radius-field);
}
&:has( > .checkbox, > .toggle, > .badge) {
--aura-radius: var(--radius-selector);
}
&:before, &:after {
animation: inherit;
background-color: inherit;
background-image: inherit;
border-radius: inherit;
position: absolute;
top: calc(1 / 2 * 100%);
left: calc(1 / 2 * 100%);
z-index: 0;
display: block;
opacity: 70%;
filter: blur(0.25rem);
translate: -50% -50%;
width: 100%;
height: 100%;
content: "";
}
&:after {
opacity: 30%;
filter: blur(1rem);
}
& > * {
position: relative;
z-index: 1;
}
}
}
.steps {
@layer daisyui.l1.l2.l3 {
display: inline-grid;
@@ -2277,49 +2175,6 @@
white-space: nowrap;
border-width: 0;
}
.menu-horizontal {
@layer daisyui.l1.l2 {
display: inline-flex;
flex-direction: row;
align-items: center;
& > li:not(.menu-title) > details {
& > :is(ul, menu) {
position: absolute;
margin-inline-start: 0;
margin-top: calc(0.25rem * 4);
transform-origin: top;
border-radius: var(--radius-box);
background-color: var(--color-base-100);
padding-block: calc(0.25rem * 2);
padding-inline-end: calc(0.25rem * 2);
opacity: 0%;
scale: 95%;
box-shadow: 0 1px 3px 0 oklch(0% 0 0/0.1), 0 1px 2px -1px oklch(0% 0 0/0.1);
@media (prefers-reduced-motion: no-preference) {
@starting-style {
scale: 95%;
opacity: 0;
}
animation: menu 0.2s;
transition-property: opacity, scale, display;
transition-behavior: allow-discrete;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
}
&[open] > :is(ul, menu) {
opacity: 100%;
scale: 100%;
}
}
& > li > details > :is(ul, menu) {
&:before {
--tw-content: none;
content: var(--tw-content);
}
}
}
}
.avatar {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -2487,48 +2342,6 @@
}
}
}
.rating {
@layer daisyui.l1.l2.l3 {
position: relative;
display: inline-flex;
vertical-align: middle;
--size: var(--size-selector, 0.25rem) * 6;
input {
cursor: pointer;
appearance: none;
}
* {
border-radius: 0;
background-color: var(--color-base-content);
opacity: 20%;
width: calc(var(--size) * 1);
height: calc(var(--size));
@media (prefers-reduced-motion: no-preference) {
animation: rating 0.25s ease-out;
}
}
.rating-hidden {
width: calc(0.25rem * 2);
background-color: transparent;
}
:checked, [aria-checked="true"], [aria-current="true"], :has( ~ :checked, ~ [aria-checked="true"], ~ [aria-current="true"]) {
opacity: 100%;
}
:focus-visible {
scale: 1.1;
@media (prefers-reduced-motion: no-preference) {
transition: scale 0.2s ease-out;
}
}
:active:focus {
animation: none;
scale: 1.1;
}
}
@layer daisyui.l1.l2 {
--size: var(--size-selector, 0.25rem) * 6;
}
}
.navbar {
@layer daisyui.l1.l2.l3 {
display: flex;
@@ -2543,6 +2356,14 @@
}
}
}
.drawer {
@layer daisyui.l1.l2.l3 {
position: relative;
display: grid;
width: 100%;
grid-auto-columns: max-content auto;
}
}
.card {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -2668,30 +2489,6 @@
.sticky {
position: sticky;
}
.dropdown-right {
@layer daisyui.l1.l2 {
--anchor-h: right;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-start: 100%;
top: 0;
bottom: auto;
transform-origin: 0;
}
}
}
.dropdown-left {
@layer daisyui.l1.l2 {
--anchor-h: left;
--anchor-v: span-bottom;
.dropdown-content {
inset-inline-end: 100%;
top: 0;
bottom: auto;
transform-origin: 100%;
}
}
}
.dropdown-end {
@layer daisyui.l1.l2 {
--anchor-h: span-left;
@@ -2961,25 +2758,6 @@
}
}
}
.btn-active {
@layer daisyui.l1.l2 {
--btn-bg: var(--btn-color, var(--color-base-200));
color: var(--btn-fg, var(--color-base-content));
--btn-border: var(--btn-bg);
@supports (color: color-mix(in lab, red, red)) {
--btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%));
}
--btn-border-style: solid;
--btn-inset: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%));
--btn-shadow: 0 3px 2px -2px var(--btn-bg),
0 4px 3px -2px var(--btn-bg);
@supports (color: color-mix(in lab, red, red)) {
--btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000),
0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000);
}
isolation: isolate;
}
}
.stack {
@layer daisyui.l1.l2.l3 {
display: inline-grid;
@@ -3159,25 +2937,8 @@
.z-10 {
z-index: 10;
}
.tab-content {
@layer daisyui.l1.l2.l3 {
order: var(--tabcontent-order);
display: none;
border-color: transparent;
--tabcontent-radius-ss: var(--radius-box);
--tabcontent-radius-se: var(--radius-box);
--tabcontent-radius-es: var(--radius-box);
--tabcontent-radius-ee: var(--radius-box);
--tabcontent-order: 1;
width: 100%;
height: calc(100% - var(--tab-height) + var(--border));
margin: var(--tabcontent-margin);
border-width: var(--border);
border-start-start-radius: var(--tabcontent-radius-ss);
border-start-end-radius: var(--tabcontent-radius-se);
border-end-start-radius: var(--tabcontent-radius-es);
border-end-end-radius: var(--tabcontent-radius-ee);
}
.z-50 {
z-index: 50;
}
.col-span-full {
grid-column: 1 / -1;
@@ -3452,17 +3213,6 @@
padding-inline: calc(var(--size) / 2 - var(--border));
}
}
.tabs {
@layer daisyui.l1.l2.l3 {
display: flex;
flex-wrap: wrap;
--tabs-height: auto;
--tabs-direction: row;
--tab-height: calc(var(--size-field, 0.25rem) * 10);
height: var(--tabs-height);
flex-direction: var(--tabs-direction);
}
}
.footer {
@layer daisyui.l1.l2.l3 {
display: grid;
@@ -3658,15 +3408,32 @@
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.table {
display: table;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
.modal-start {
@layer daisyui.l1.l2 {
place-items: start;
.modal-box {
height: 100vh;
max-height: none;
width: auto;
max-width: none;
translate: -100% 0;
scale: 1;
--modal-tl: 0;
--modal-tr: var(--radius-box);
--modal-bl: 0;
--modal-br: var(--radius-box);
[dir="rtl"] & {
translate: 100% 0;
}
}
}
}
.btn-circle {
@layer daisyui.l1.l2 {
border-radius: calc(infinity * 1px);
@@ -3675,16 +3442,23 @@
height: var(--size);
}
}
.btn-square {
@layer daisyui.l1.l2 {
padding-inline: 0;
width: var(--size);
height: var(--size);
}
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.h-12 {
height: calc(var(--spacing) * 12);
}
.h-16 {
height: calc(var(--spacing) * 16);
}
.h-56 {
height: calc(var(--spacing) * 56);
}
@@ -3700,30 +3474,33 @@
.max-h-60 {
max-height: calc(var(--spacing) * 60);
}
.max-h-none {
max-height: none;
}
.min-h-6 {
min-height: calc(var(--spacing) * 6);
}
.w-10 {
width: calc(var(--spacing) * 10);
}
.w-12 {
width: calc(var(--spacing) * 12);
}
.w-16 {
width: calc(var(--spacing) * 16);
}
.w-20 {
width: calc(var(--spacing) * 20);
}
.w-24 {
width: calc(var(--spacing) * 24);
}
.w-28 {
width: calc(var(--spacing) * 28);
}
.w-60 {
width: calc(var(--spacing) * 60);
}
.w-64 {
width: calc(var(--spacing) * 64);
}
.w-72 {
width: calc(var(--spacing) * 72);
}
.w-full {
width: 100%;
}
@@ -3736,6 +3513,9 @@
.max-w-40 {
max-width: calc(var(--spacing) * 40);
}
.max-w-\[85vw\] {
max-width: 85vw;
}
.max-w-md {
max-width: var(--container-md);
}
@@ -3754,61 +3534,21 @@
.min-w-40 {
min-width: calc(var(--spacing) * 40);
}
.min-w-64 {
min-width: calc(var(--spacing) * 64);
}
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.skeleton {
@layer daisyui.l1.l2.l3 {
border-radius: var(--radius-box);
background-color: var(--color-base-300);
@media (prefers-reduced-motion: reduce) {
transition-duration: 15s;
}
will-change: background-position;
background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% );
background-size: 200% auto;
background-position-x: -50%;
@media (prefers-reduced-motion: no-preference) {
animation: skeleton 1.8s ease-in-out infinite;
}
}
}
.aura-glow {
@layer daisyui.l1.l2 {
animation: none;
background-image: radial-gradient(closest-corner at center, currentColor 0%, transparent 90%);
&:before {
animation: aura-glow var(--tw-duration, 6s) ease-out infinite;
}
&:after {
animation: aura-glow-after var(--tw-duration, 6s) ease-out infinite;
}
}
}
.animate-pulse {
animation: var(--animate-pulse);
}
@@ -3884,9 +3624,6 @@
.justify-start {
justify-content: flex-start;
}
.justify-items-start {
justify-items: start;
}
.gap-1 {
gap: var(--spacing);
}
@@ -3916,9 +3653,6 @@
.gap-x-4 {
column-gap: calc(var(--spacing) * 4);
}
.gap-x-6 {
column-gap: calc(var(--spacing) * 6);
}
.gap-y-2 {
row-gap: calc(var(--spacing) * 2);
}
@@ -3968,6 +3702,9 @@
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-none {
border-radius: 0;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -4141,15 +3878,15 @@
--btn-shadow: 0 0 0 0 oklch(0% 0 0/0);
}
}
.mask-repeat {
mask-repeat: repeat;
}
.object-contain {
object-fit: contain;
}
.object-cover {
object-fit: cover;
}
.p-0 {
padding: 0;
}
.p-2 {
padding: calc(var(--spacing) * 2);
}
@@ -4282,6 +4019,10 @@
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.font-normal {
--tw-font-weight: var(--font-weight-normal);
font-weight: var(--font-weight-normal);
}
.font-semibold {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
@@ -4294,9 +4035,6 @@
--tw-tracking: var(--tracking-wider);
letter-spacing: var(--tracking-wider);
}
.text-wrap {
text-wrap: wrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
@@ -4413,9 +4151,6 @@
text-decoration-line: none;
}
}
.underline {
text-decoration-line: underline;
}
.opacity-40 {
opacity: 40%;
}
@@ -4472,19 +4207,6 @@
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.input-lg {
@layer daisyui.l1.l2 {
--in-size-mul: 12;
@@ -4664,6 +4386,11 @@
grid-column: span 2 / span 2;
}
}
.sm\:block {
@media (width >= 40rem) {
display: block;
}
}
.sm\:hidden {
@media (width >= 40rem) {
display: none;
@@ -4674,6 +4401,36 @@
display: inline;
}
}
.sm\:inline-flex {
@media (width >= 40rem) {
display: inline-flex;
}
}
.sm\:h-16 {
@media (width >= 40rem) {
height: calc(var(--spacing) * 16);
}
}
.sm\:w-16 {
@media (width >= 40rem) {
width: calc(var(--spacing) * 16);
}
}
.sm\:w-auto {
@media (width >= 40rem) {
width: auto;
}
}
.sm\:max-w-xs {
@media (width >= 40rem) {
max-width: var(--container-xs);
}
}
.sm\:grow-0 {
@media (width >= 40rem) {
flex-grow: 0;
}
}
.sm\:grid-cols-2 {
@media (width >= 40rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4684,34 +4441,51 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sm\:flex-row {
@media (width >= 40rem) {
flex-direction: row;
}
}
.sm\:flex-wrap {
@media (width >= 40rem) {
flex-wrap: wrap;
}
}
.sm\:items-center {
@media (width >= 40rem) {
align-items: center;
}
}
.sm\:justify-between {
@media (width >= 40rem) {
justify-content: space-between;
}
}
.sm\:gap-3 {
@media (width >= 40rem) {
gap: calc(var(--spacing) * 3);
}
}
.sm\:gap-4 {
@media (width >= 40rem) {
gap: calc(var(--spacing) * 4);
}
}
.sm\:px-6 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 6);
}
}
.md\:col-span-2 {
@media (width >= 48rem) {
grid-column: span 2 / span 2;
.sm\:text-2xl {
@media (width >= 40rem) {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
}
.md\:col-span-4 {
@media (width >= 48rem) {
grid-column: span 4 / span 4;
}
}
.md\:mb-2 {
@media (width >= 48rem) {
margin-bottom: calc(var(--spacing) * 2);
}
}
.md\:grid {
@media (width >= 48rem) {
display: grid;
}
}
.md\:hidden {
@media (width >= 48rem) {
display: none;
.sm\:text-xl {
@media (width >= 40rem) {
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
}
.md\:grid-cols-2 {
@@ -4734,24 +4508,9 @@
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
.md\:grid-cols-\[minmax\(0\,1fr\)_2\.5rem\] {
@media (width >= 48rem) {
grid-template-columns: minmax(0,1fr) 2.5rem;
}
}
.md\:grid-cols-\[minmax\(0\,2fr\)_minmax\(0\,2fr\)_7rem_2\.5rem\] {
@media (width >= 48rem) {
grid-template-columns: minmax(0,2fr) minmax(0,2fr) 7rem 2.5rem;
}
}
.md\:items-start {
@media (width >= 48rem) {
align-items: flex-start;
}
}
.md\:justify-center {
@media (width >= 48rem) {
justify-content: center;
.lg\:col-span-1 {
@media (width >= 64rem) {
grid-column: span 1 / span 1;
}
}
.lg\:block {
@@ -4759,16 +4518,36 @@
display: block;
}
}
.lg\:contents {
@media (width >= 64rem) {
display: contents;
}
}
.lg\:flex {
@media (width >= 64rem) {
display: flex;
}
}
.lg\:hidden {
@media (width >= 64rem) {
display: none;
}
}
.lg\:w-auto {
@media (width >= 64rem) {
width: auto;
}
}
.lg\:w-fit {
@media (width >= 64rem) {
width: fit-content;
}
}
.lg\:grow-0 {
@media (width >= 64rem) {
flex-grow: 0;
}
}
.lg\:grid-cols-2 {
@media (width >= 64rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4789,6 +4568,21 @@
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
.lg\:flex-row {
@media (width >= 64rem) {
flex-direction: row;
}
}
.lg\:flex-wrap {
@media (width >= 64rem) {
flex-wrap: wrap;
}
}
.lg\:items-center {
@media (width >= 64rem) {
align-items: center;
}
}
.xl\:grid-cols-3 {
@media (width >= 80rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -4933,6 +4727,46 @@
backface-visibility: hidden;
transform: translateZ(0);
}
@media (width < 48rem) {
.table-cards, .table-cards :is(thead, tbody, tr, th, td) {
display: block;
width: 100%;
}
.table-cards thead {
display: none;
}
.table-cards tbody tr {
border: 1px solid var(--color-base-300);
border-radius: var(--radius-box);
padding: 0 0.75rem;
}
.table-cards tbody tr + tr {
margin-top: 0.75rem;
}
.table-cards td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--color-base-200);
}
.table-cards td:last-child {
border-bottom: none;
}
.table-cards td[data-label]:before {
content: attr(data-label);
flex-shrink: 0;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
opacity: 0.5;
}
.table-cards td:not([data-label]) {
justify-content: flex-start;
}
}
@layer base {
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
color-scheme: light;
@@ -5219,26 +5053,6 @@
opacity: 0;
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
@@ -5405,10 +5219,6 @@
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@keyframes pulse {
50% {
opacity: 0.5;
@@ -5417,11 +5227,6 @@
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-space-y-reverse: 0;
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
@@ -5460,7 +5265,6 @@
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
--tw-ease: initial;
}
}
}

View File

@@ -32,8 +32,28 @@
input.placeholder = select.dataset.searchPlaceholder || "";
input.autocomplete = "off";
// `fixed`, not `absolute`: an absolutely positioned dropdown is clipped by
// the nearest scrolling ancestor (e.g. the overflow-x-auto wrapper the
// bulk-add tables would otherwise need on mobile), which is exactly what
// stopped those tables from getting one. Fixed positioning is relative to
// the viewport regardless of ancestors, so it's never clipped that way --
// position and size are computed in JS instead of via CSS, see positionList().
const list = document.createElement("ul");
list.className = "menu absolute z-10 mt-1 w-full rounded-box bg-base-100 shadow max-h-60 overflow-y-auto flex-nowrap hidden";
list.className = "menu fixed z-50 rounded-box bg-base-100 shadow max-h-60 overflow-y-auto flex-nowrap hidden";
const positionList = () => {
const rect = input.getBoundingClientRect();
list.style.left = `${rect.left}px`;
list.style.top = `${rect.bottom + 4}px`;
list.style.width = `${rect.width}px`;
};
// Capture phase: a "scroll" event on an inner scroll container (the
// overflow-x-auto table wrapper) doesn't bubble up to window in the
// normal (bubbling) phase, but every scroll is seen on the way down in
// the capture phase, so this still fires no matter which ancestor moved.
window.addEventListener("scroll", () => { if (!list.classList.contains("hidden")) positionList(); }, true);
window.addEventListener("resize", () => { if (!list.classList.contains("hidden")) positionList(); });
select.parentNode.insertBefore(wrapper, select);
if (isMultiple) {
@@ -115,7 +135,12 @@
});
highlighted = -1;
list.classList.toggle("hidden", matches.length === 0);
if (matches.length === 0) {
list.classList.add("hidden");
} else {
positionList();
list.classList.remove("hidden");
}
return matches;
};

View File

@@ -53,10 +53,27 @@
intrinsic width.
{% endcomment %}
<div class="navbar shrink-0 border-b border-base-300 bg-base-100 px-3 shadow-sm sm:px-6">
<div class="my-4 min-w-0 flex-1">
<div class="my-4 flex min-w-0 flex-1 items-center gap-1">
{% comment %}
Empty for pages with no menu (the auth screens) -- management/base.html
and controlpanel/base.html are the only overriders, each pairing this
with its own `{% block mobile_menu %}` dialog below in `main`.
{% endcomment %}
{% block nav_toggle %}{% endblock nav_toggle %}
<div class="min-w-0">
{% block brand %}{% endblock brand %}
</div>
</div>
{% comment %}
Hidden below `lg` wherever a hamburger drawer exists to hold them instead
(management/base.html, controlpanel/base.html override `nav_icons_class` to
`hidden lg:flex` and duplicate these same three controls inside their
{% block nav_toggle %} dialog) -- on a page with no such drawer (the auth
screens, the public club site) the default keeps them in the navbar itself,
since there'd be nowhere else for them to go.
{% endcomment %}
<div class="{% block nav_icons_class %}flex items-center{% endblock nav_icons_class %}">
<button class="btn btn-ghost" type="button" data-theme-toggle aria-label="Theme">
<span data-theme-icon="light" class="hidden items-center gap-4">{% lucide "sun" size=20 %} <span class="hidden sm:inline">light</span></span>
<span data-theme-icon="dark" class="hidden items-center gap-4">{% lucide "moon" size=20 %} <span class="hidden sm:inline">dark</span></span>
@@ -72,7 +89,10 @@
{% if user.is_superuser %}
<a class="btn btn-ghost gap-4" href="{% url "admin:index" %}" aria-label="Django admin">{% lucide "shield-cog" %}<span class="hidden sm:inline">Django admin</span></a>
{% endif %}
{% endif %}
</div>
{% if user.is_authenticated %}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost gap-4" aria-label="Account">{% lucide "circle-user" %}<span class="hidden max-w-40 truncate sm:inline">{{ user.get_full_name }}</span></div>
<ul tabindex="0" class="menu dropdown-content z-10 mt-2 w-60 rounded-box bg-base-100 p-2 shadow border border-base-content/20">

View File

@@ -34,25 +34,26 @@
{% endblock extra %}
{% block brand %}
<a class="flex flex-row items-center gap-3" href="/">
{# min-w-0 on both the link and the text column: without it a flex child's default min-width:auto refuses to shrink past the club name's intrinsic width, so `truncate` never gets the chance to kick in and the name pushes the header wider than the viewport instead of ellipsizing. The avatar and tagline shrink/hide below `sm` for the same reason -- a long club name at full size has nowhere left to give. #}
<a class="flex min-w-0 flex-row items-center gap-2 sm:gap-3" href="/">
{% if club.logo %}
{# The ring is the club's own primary colour -- a plain frame around whatever crest they uploaded. #}
<div class="avatar">
<div class="w-16 rounded-full bg-base-100 ring-2 ring-primary ring-offset-2 ring-offset-base-100">
<div class="avatar shrink-0">
<div class="w-10 rounded-full bg-base-100 ring-2 ring-primary ring-offset-2 ring-offset-base-100 sm:w-16">
<img class="club-logo object-contain" src="{{ club.logo.url }}" alt="{{ club.name }}">
</div>
</div>
{% else %}
{# Never the RosterChief mark: that would pass our branding off as the club's own. #}
<div class="avatar avatar-placeholder">
<div class="w-16 rounded-full bg-secondary text-secondary-content">
<span class="font-roboto text-xl font-bold">{{ club.initials }}</span>
<div class="avatar avatar-placeholder shrink-0">
<div class="w-10 rounded-full bg-secondary text-secondary-content sm:w-16">
<span class="font-roboto text-sm font-bold sm:text-xl">{{ club.initials }}</span>
</div>
</div>
{% endif %}
<div class="flex flex-col gap-1">
<div class="font-roboto text-2xl font-bold tracking-wider">{{ club.name }}</div>
<div class="font-mono text-xs font-semibold text-base-content/50">Powered by RosterChief</div>
<div class="flex min-w-0 flex-col gap-1">
<div class="truncate font-roboto text-lg font-bold tracking-wider sm:text-2xl">{{ club.name }}</div>
<div class="hidden truncate font-mono text-xs font-semibold text-base-content/50 sm:block">Powered by RosterChief</div>
</div>
</a>
{% endblock brand %}

View File

@@ -25,11 +25,11 @@
{% endblock extra %}
{% block brand %}
<a class="flex flex-row items-center gap-2" href="/">
<span class="logo inline-block h-16 w-16" role="img" aria-label="RosterChief"></span>
<div class="flex flex-col gap-1">
<div class="font-roboto text-2xl font-bold tracking-wider">Roster<span class="text-sky-500">Chief</span></div>
<div class="font-mono text-xs font-semibold text-base-content/50">Club &amp; Team Management</div>
<a class="flex min-w-0 flex-row items-center gap-2" href="/">
<span class="logo inline-block h-10 w-10 shrink-0 sm:h-16 sm:w-16" role="img" aria-label="RosterChief"></span>
<div class="flex min-w-0 flex-col gap-1">
<div class="truncate font-roboto text-lg font-bold tracking-wider sm:text-2xl">Roster<span class="text-sky-500">Chief</span></div>
<div class="hidden truncate font-mono text-xs font-semibold text-base-content/50 sm:block">Club &amp; Team Management</div>
</div>
</a>
{% endblock brand %}