Files
RosterChief/management/templates/management/event_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

202 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 event" %}{% 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." %}</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 "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 & when" %}</div>
<div class="event-section-hint">{% trans "Venue and the times members see on their invite." %}</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.start %}
{% form_field form.end %}
{% form_field form.gathering %}
{% form_field form.deadline %}
</div>
</div>
</div>
<div id="game-details-section" class="card game-only">
<div class="card-body">
<div class="event-section-header">
<span class="event-section-icon event-section-icon-accent">{% lucide "swords" size=18 %}</span>
<div>
<div class="event-section-title">{% trans "Game details" %}</div>
<div class="event-section-hint">{% trans "Only used for games -- competition, referees and, once played, the score." %}</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.competition %}
{% form_field form.external_game_id %}
{% form_field form.max_referees %}
</div>
{% if update_view %}
{% comment %}
Score/live status only exist to record once a game has
actually been created -- the add form leaves them out
entirely (see EventForm's editing=False path).
{% endcomment %}
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.score_for %}
{% form_field form.score_against %}
{% form_field form.is_live show_as_toggle=True %}
</div>
{% endif %}
</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_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>
(() => {
// Progressive enhancement only -- every field still posts normally
// either way. The kind cards are the real "kind" radios (see the
// template) -- selecting one is what drives (a) the .game-only
// show/hide (opponent, and the whole Game details card -- anything
// that only makes sense for a game carries this class) and (b) the
// live summary line up top. No JS is needed to keep a picker in sync
// with a hidden field, because there isn't one.
const kindInputs = document.querySelectorAll('input[name="kind"]');
const titleField = document.getElementById("id_title");
const startField = document.getElementById("id_start");
const gameOnlyElements = document.querySelectorAll(".game-only");
const summary = document.getElementById("event-summary-text");
if (!kindInputs.length) return;
const NO_TITLE = "{% trans "Untitled event" %}";
const NO_DATE = "{% trans "no 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 when = formatStart(startField ? startField.value : "");
summary.textContent = `${kindLabel} ${title} ${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 (startField) startField.addEventListener("input", updateSummary);
sync();
})();
</script>
{% endblock extra_body %}