Make the whole page scroll, show referee sign-up on the event page too

The app shell now scrolls as one page (body/document) instead of boxing
content inside an inner overflow-auto <main> under a fixed header/tab bar --
header and tab bar are sticky instead, which reads as a single intuitive
scroll (most noticeable on Calendar's long agenda) and, as a side effect,
means htmx's boosted-navigation scroll-to-top actually resets what the user
sees. The inner "This week"/"Today" sticky sub-headers are no longer sticky
themselves, since they'd otherwise collide with the now-sticky app header.

Event detail also shows the same referee Accept/Decline card the Calendar
row offers, for a signed-in account with a pending or accepted sign-up on
that game -- not just on the Calendar. Both the calendar row's and the
event page's Accept/Decline forms are now htmx-boosted (no hx-boost="false")
since neither is Alpine-toggled, so a response no longer forces a full page
reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-22 18:08:17 +02:00
parent 875250366e
commit e349fbbf03
9 changed files with 133 additions and 26 deletions

View File

@@ -395,12 +395,15 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
class RefereeSignupRespondView(PersonScopeMixin, LoginRequiredMixin, View):
"""Accept/decline a referee invite from a Calendar row
(mobile/_calendar_referee_row.html) -- routes through
events.services.referees.accept_referee_signup/decline_referee_signup,
so capacity is enforced in the one place the desktop admin flow already
enforces it, and the referee-management screen sees the result with no
separate sync step."""
"""Accept/decline a referee invite -- from a Calendar row
(mobile/_calendar_referee_row.html) or the same event's own detail page
(event_detail.html's own "Refereeing" card, for the same signup). Routes
through events.services.referees.accept_referee_signup/
decline_referee_signup, so capacity is enforced in the one place the
desktop admin flow already enforces it, and the referee-management
screen sees the result with no separate sync step. Boosted (no explicit
hx-boost="false") -- unlike event_detail's own RSVP forms, nothing here
is Alpine-owned/toggled, so there's no htmx/Alpine conflict to dodge."""
def post(self, request, *args, **kwargs):
signup = get_object_or_404(RefereeSignup, pk=kwargs["signup_id"], member=self.me, event__club=request.club)
@@ -418,6 +421,8 @@ class RefereeSignupRespondView(PersonScopeMixin, LoginRequiredMixin, View):
else:
return HttpResponseBadRequest(_("Unknown response."))
if request.POST.get("next") == "event_detail":
return HttpResponseRedirect(reverse("mobile:event_detail", kwargs={"pk": signup.event_id}))
return HttpResponseRedirect(reverse("mobile:calendar"))
@@ -456,6 +461,14 @@ class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
lineup = Lineup.objects.filter(event=event, published_at__isnull=False).first()
lineup_categories = selected_members_by_position(lineup) if lineup is not None else []
# Same self.me-only scope as the Calendar row this mirrors
# (mobile/_calendar_referee_row.html) -- a referee is an adult
# acting on their own behalf, never something a parent does for a
# managed child. Declined signups are excluded -- nothing left to do.
referee_signup = None
if self.me is not None:
referee_signup = RefereeSignup.objects.filter(event=event, member=self.me, status__in=[RefereeSignup.Status.INVITED, RefereeSignup.Status.ACCEPTED]).first()
your_answers = []
if self.managed_people:
managed_ids = [person.pk for person in self.managed_people]
@@ -494,7 +507,17 @@ class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
"no_reply_pct": round(100 * counts["no_reply_count"] / total),
}
return super().get_context_data(screen_title=event.title, event=event, rsvp_closed=rsvp_closed, lineup=lineup, lineup_categories=lineup_categories, your_answers=your_answers, squad_summary=squad_summary, **kwargs)
return super().get_context_data(
screen_title=event.title,
event=event,
rsvp_closed=rsvp_closed,
lineup=lineup,
lineup_categories=lineup_categories,
referee_signup=referee_signup,
your_answers=your_answers,
squad_summary=squad_summary,
**kwargs,
)
def post(self, request, *args, **kwargs):
status = request.POST.get("status")