Files
RosterChief/management/templates/management/event_series_form.html
Bernard Siebens 441367362d Build the D5 events calendar, redesign event forms, and polish the events UI
Adds the Week/Month/Season calendar to the Events page (team/group-scoped
visibility, kind-colored blocks, a repeating-series indicator) alongside the
existing table as a List view, and redesigns event/event-series creation
around a kind-first picker with progressive disclosure instead of one long
flat form. Also: an accessible radio-based kind picker (no redundant
select-next-to-cards), a fixed week-grid layout bug, a filterable single-list
redesign of the event detail page's RSVP/responses modal, a tidied-up
recurrence field layout, and assorted button/field height and width fixes
across the referee panel and event forms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
2026-08-20 01:08:53 +02:00

201 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "management/base.html" %}
{% load i18n lucide static ui %}
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New series" %}{% endif %}{% endblock heading %}
{% block panel %}
<form method="post" class="flex flex-col gap-4">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="card">
<div class="card-body">
<div class="event-section-header">
<span class="event-section-icon">{% lucide "clipboard-list" size=18 %}</span>
<div>
<div class="event-section-title">{% trans "What's happening?" %}</div>
<div class="event-section-hint">{% trans "Pick a kind to reveal only the fields that matter for it -- it applies to every occurrence this series generates." %}</div>
</div>
</div>
<div class="mb-4">
{# Real radios, not a JS-synced facade over a hidden <select> -- each card IS the field's own input, so keyboard/tab/arrow-key operation and screen readers get native radiogroup semantics for free, and there's no second visible control for the same field. #}
<div class="kind-grid" role="radiogroup" aria-label="{{ form.fields.kind.label|capfirst }}">
{% for value, label in form.fields.kind.choices %}
<label class="kind-card" data-kind="{{ value }}">
<input type="radio" name="{{ form.kind.html_name }}" value="{{ value }}" class="sr-only" {% if form.kind.value == value %}checked{% endif %} required>
{% if value == "training" %}{% lucide "dumbbell" size=20 %}
{% elif value == "game" %}{% lucide "swords" size=20 %}
{% elif value == "tournament" %}{% lucide "trophy" size=20 %}
{% elif value == "meeting" %}{% lucide "message-square" size=20 %}
{% elif value == "social" %}{% lucide "party-popper" size=20 %}
{% elif value == "other" %}{% lucide "circle-ellipsis" size=20 %}
{% else %}{% lucide "calendar" size=20 %}
{% endif %}
<span class="kind-card-label">{{ label }}</span>
</label>
{% endfor %}
</div>
{% if form.kind.errors %}
<p class="mt-2 text-xs text-club-dark">{{ form.kind.errors|join:" " }}</p>
{% endif %}
</div>
<div class="w-full">
{% form_field form.title %}
</div>
</div>
</div>
<div class="card event-summary">
<span class="event-summary-icon">{% lucide "sparkles" size=16 %}</span>
<span id="event-summary-text">{% trans "This line updates live as you fill in the form below." %}</span>
</div>
<div class="card">
<div class="card-body">
<div class="event-section-header">
<span class="event-section-icon">{% lucide "repeat" size=18 %}</span>
<div>
<div class="event-section-title">{% trans "Repeats" %}</div>
<div class="event-section-hint">{% trans "The friendly pattern below, or drop into a raw recurrence rule for anything unusual." %}</div>
</div>
</div>
{% trans "Only used for a weekly pattern -- a monthly one repeats on the same day of the month as the first occurrence below." as weekdays_help %}
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.frequency %}
{% form_field form.interval %}
{% form_field form.weekdays help_text=weekdays_help %}
{% form_field form.dtstart %}
{% form_field form.until %}
</div>
<details class="collapse-arrow mt-3 rounded-lg border border-line bg-white">
<summary class="cursor-pointer px-3 py-2 text-sm font-medium text-ink">{% trans "Advanced: raw recurrence rule" %}</summary>
<div class="border-t border-rule p-3">
{% form_field form.advanced_rrule %}
</div>
</details>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="event-section-header">
<span class="event-section-icon">{% lucide "users" size=18 %}</span>
<div>
<div class="event-section-title">{% trans "Who's invited" %}</div>
<div class="event-section-hint">{% trans "Teams and groups invite every current member automatically." %}</div>
</div>
</div>
{% if "club_wide" in form.fields %}
<div class="event-clubwide-toggle">
{% form_field form.club_wide show_as_toggle=True %}
</div>
{% endif %}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.teams %}
{% form_field form.groups %}
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.invited_members %}
{% form_field form.excluded_members %}
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="event-section-header">
<span class="event-section-icon">{% lucide "calendar-clock" size=18 %}</span>
<div>
<div class="event-section-title">{% trans "Where & timing" %}</div>
<div class="event-section-hint">{% trans "Venue, and how long each occurrence runs relative to its start." %}</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.location %}
<div class="game-only">
{% form_field form.opponent %}
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 border-t border-rule pt-4 md:grid-cols-4">
{% form_field form.duration_hours %}
{% form_field form.duration_minutes %}
{% form_field form.gathering_minutes_before %}
{% form_field form.deadline_minutes_before %}
</div>
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:event_series_detail" object.pk %}{% else %}{% url "management:event_list" %}{% endif %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
<button class="btn btn-primary gap-2" type="submit">{% lucide "save" size=16 %} {% trans "Save" %}</button>
</div>
</form>
{% endblock panel %}
{% block extra_body %}
<script src="{% static 'js/searchable-select.js' %}"></script>
<script>
(() => {
// Same mechanism as event_form.html's own copy of this script (see
// its comment) -- the kind cards are the real "kind" radios, which
// drive .game-only (here just the opponent field -- a series has no
// separate "Game details" section to show/hide) and the live
// summary line. No hidden field to keep in sync with a picker.
const kindInputs = document.querySelectorAll('input[name="kind"]');
const titleField = document.getElementById("id_title");
const dtstartField = document.getElementById("id_dtstart");
const frequencyField = document.getElementById("id_frequency");
const gameOnlyElements = document.querySelectorAll(".game-only");
const summary = document.getElementById("event-summary-text");
if (!kindInputs.length) return;
const NO_TITLE = "{% trans "Untitled series" %}";
const NO_DATE = "{% trans "no start date set yet" %}";
const formatStart = (value) => {
if (!value) return NO_DATE;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return NO_DATE;
return date.toLocaleString(undefined, { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
};
const checkedKind = () => document.querySelector('input[name="kind"]:checked');
const updateSummary = () => {
if (!summary) return;
const checked = checkedKind();
const kindLabel = checked ? checked.closest(".kind-card").querySelector(".kind-card-label").textContent.trim() : "";
const title = (titleField && titleField.value.trim()) || NO_TITLE;
const selectedFrequency = frequencyField ? frequencyField.options[frequencyField.selectedIndex] : null;
const frequencyLabel = selectedFrequency ? selectedFrequency.text : "";
const when = formatStart(dtstartField ? dtstartField.value : "");
summary.textContent = `${kindLabel} ${title} ${frequencyLabel}, ${when}`;
};
const sync = () => {
const checked = checkedKind();
gameOnlyElements.forEach((element) => element.classList.toggle("hidden", !checked || checked.value !== "game"));
updateSummary();
};
kindInputs.forEach((input) => input.addEventListener("change", sync));
if (titleField) titleField.addEventListener("input", updateSummary);
if (dtstartField) dtstartField.addEventListener("input", updateSummary);
if (frequencyField) frequencyField.addEventListener("change", updateSummary);
sync();
})();
</script>
{% endblock extra_body %}