Enable hx-boost app-wide for SPA-like navigation; Me page polish
hx-boost="true" on both shells' <body> turns every same-shell link/form
into an AJAX navigation (swapping <body>'s children, pushing the URL)
instead of a full browser reload -- htmx.js was already loaded on every
page but had zero actual usage anywhere in the codebase until now.
Only <body>'s children swap under a boost, never the tag itself, so
anything that crosses between the two differently-styled shells (Member's
bg-paper vs Coach's bg-ink, different data attributes) is marked
hx-boost="false" to force a real navigation instead: both role-switcher
links, the Me page's "Teams I coach/manage" row (leads into Coach mode),
and Coach Today's "Also yours" quick-RSVP forms (post to the Member-shell
event_detail view). The calendar-sync "Add to calendar" webcal:// link is
also excluded -- it's meant to hand off to the OS calendar app, not be
treated as in-app navigation.
Also, on the Me page:
- Renamed the mode switcher and the "Teams I coach" card to "Manager"/
"Teams I coach/manage" -- a team manager assigned there isn't
necessarily a coach.
- Fixed the team badge, which was slicing short_name to 2 characters
("U16" showing as "U1") -- replaced with a plain icon instead of text,
so there's nothing left to truncate (the full team name is still the
row's own label).
- Styled the whole card dark (m-card-dark), matching the design mock's
own dark "Coach mode" promo card treatment for this part of M5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -12,6 +12,17 @@
|
||||
account with >=1 current-season staff assignment -- mobile/mixins.py's
|
||||
PersonScopeMixin) -- Coach mode (C1-C6, mobile/coach_views.py) is being
|
||||
built out screen by screen; only what's actually shipped is linked to.
|
||||
|
||||
hx-boost="true" on <body>: every same-shell link/form becomes an AJAX
|
||||
navigation (swapping <body>'s children, pushing the URL) instead of a
|
||||
full browser reload -- the SPA-like feel without an SPA. Only <body>'s
|
||||
*children* swap, not the tag itself, so anything that crosses into the
|
||||
differently-styled Coach shell (coach/base.html: bg-ink vs this page's
|
||||
bg-paper, its own hx-headers/data attrs) is marked hx-boost="false" to
|
||||
force a real navigation instead -- a boosted cross-shell swap would leave
|
||||
the OLD shell's <body> class/attributes in place under the NEW shell's
|
||||
content. See coach/base.html's own comment for the same rule mirrored
|
||||
the other direction.
|
||||
{% endcomment %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
||||
@@ -51,7 +62,7 @@
|
||||
{% block extra_head %}{% endblock extra_head %}
|
||||
</head>
|
||||
|
||||
<body class="flex h-screen flex-col overflow-hidden bg-paper font-sans text-slate" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' data-vapid-public-key="{{ vapid_public_key }}" data-csrftoken="{{ csrf_token }}">
|
||||
<body class="flex h-screen flex-col overflow-hidden bg-paper font-sans text-slate" hx-boost="true" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' data-vapid-public-key="{{ vapid_public_key }}" data-csrftoken="{{ csrf_token }}">
|
||||
<header class="app-header">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<a class="flex items-center gap-2.5" href="{% url "mobile:home" %}">
|
||||
@@ -78,7 +89,7 @@
|
||||
{% if has_coach_access %}
|
||||
<div class="role-switcher">
|
||||
<a class="role-switcher-item role-switcher-item-active" href="{% url "mobile:home" %}">{% trans "Member" %}</a>
|
||||
<a class="role-switcher-item" href="{% url "mobile:coach_today" %}">{% trans "Coach" %}</a>
|
||||
<a class="role-switcher-item" href="{% url "mobile:coach_today" %}" hx-boost="false">{% trans "Manager" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block header_extra %}{% endblock header_extra %}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<p class="text-sm text-muted">{% trans "Subscribe this link in your phone or computer's calendar app to see everyone you manage's schedule alongside your own. It includes every event they're invited to, not just the ones they've replied to." %}</p>
|
||||
|
||||
<div class="m-card p-4">
|
||||
<a class="btn btn-primary flex w-full items-center justify-center" href="{{ webcal_url }}">{% trans "Add to calendar" %}</a>
|
||||
<a class="btn btn-primary flex w-full items-center justify-center" href="{{ webcal_url }}" hx-boost="false">{% trans "Add to calendar" %}</a>
|
||||
<p class="mt-2 text-center text-xs text-dim">{% trans "Opens directly in Apple/macOS Calendar. On Android or a desktop calendar app, copy the link below and add it as a new calendar by URL." %}</p>
|
||||
|
||||
<div class="mt-4" x-data="{ copied: false }">
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
mobile:icon/mobile:service_worker PWA plumbing as Member mode -- one app, one
|
||||
manifest, two modes, not two separate PWAs.
|
||||
|
||||
The tab bar only ever links to screens that actually exist. Today (C1) is the only
|
||||
coach screen built so far, so it's the only coach item -- Me reuses the *existing*
|
||||
member mobile:me page rather than a separate coach-Me screen (see CoachTodayView's
|
||||
own docstring). No dead links: as C2/C4/C5/C6 land, they get their own tab/entry
|
||||
point then, not stubbed in ahead of time (same principle mobile/templates/mobile/
|
||||
me.html's own comment already applies to "Household & contacts"/"Coach mode" before
|
||||
this existed).
|
||||
The tab bar deliberately stays minimal (Today + Me) even with all six coach screens
|
||||
built -- C2/C3/C4/C5/C6 are reached from Today's own action buttons and "needs you"
|
||||
list, not separate tab items; Me reuses the *existing* member mobile:me page rather
|
||||
than a separate coach-Me screen (see CoachTodayView's own docstring).
|
||||
|
||||
hx-boost="true" on <body>: every same-shell link/form becomes an AJAX navigation
|
||||
instead of a full browser reload -- see mobile/templates/mobile/base.html's own
|
||||
comment for the full reasoning. Only <body>'s children swap, not the tag itself, so
|
||||
anything that crosses back into the Member shell (bg-paper, its own data attrs) is
|
||||
marked hx-boost="false" to force a real navigation instead.
|
||||
{% endcomment %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
||||
@@ -54,7 +57,7 @@
|
||||
{% block extra_head %}{% endblock extra_head %}
|
||||
</head>
|
||||
|
||||
<body class="flex h-screen flex-col overflow-hidden bg-ink font-sans text-slate" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' data-csrftoken="{{ csrf_token }}">
|
||||
<body class="flex h-screen flex-col overflow-hidden bg-ink font-sans text-slate" hx-boost="true" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' data-csrftoken="{{ csrf_token }}">
|
||||
<header class="coach-header">
|
||||
<div class="flex items-center gap-2.5">
|
||||
{% if club.logo %}
|
||||
@@ -79,8 +82,8 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="role-switcher">
|
||||
<a class="role-switcher-item" href="{% url "mobile:home" %}">{% trans "Member" %}</a>
|
||||
<a class="role-switcher-item role-switcher-item-active" href="{% url "mobile:coach_today" %}">{% trans "Coach" %}</a>
|
||||
<a class="role-switcher-item" href="{% url "mobile:home" %}" hx-boost="false">{% trans "Member" %}</a>
|
||||
<a class="role-switcher-item role-switcher-item-active" href="{% url "mobile:coach_today" %}">{% trans "Manager" %}</a>
|
||||
</div>
|
||||
|
||||
{% block header_extra %}{% endblock header_extra %}
|
||||
|
||||
@@ -86,14 +86,21 @@
|
||||
{% if rsvp_closed %}
|
||||
<span class="pill pill-neutral mt-3">{{ hero_attendance.get_status_display }}</span>
|
||||
{% else %}
|
||||
{% comment %}
|
||||
hx-boost="false" -- mobile:event_detail is a Member-shell view
|
||||
(bg-paper, not this page's bg-ink); boosting would swap its
|
||||
response into this page's <body> without updating the <body>
|
||||
tag's own class, leaving the wrong background. See base.html's
|
||||
own comment for the full reasoning.
|
||||
{% endcomment %}
|
||||
<div class="mt-3 flex gap-2">
|
||||
<form class="flex-1" method="post" action="{% url "mobile:event_detail" hero_attendance.event.pk %}">
|
||||
<form class="flex-1" method="post" action="{% url "mobile:event_detail" hero_attendance.event.pk %}" hx-boost="false">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
||||
<input type="hidden" name="status" value="present">
|
||||
<button class="btn btn-positive w-full" type="submit">{% trans "In" %}</button>
|
||||
</form>
|
||||
<form class="flex-1" method="post" action="{% url "mobile:event_detail" hero_attendance.event.pk %}">
|
||||
<form class="flex-1" method="post" action="{% url "mobile:event_detail" hero_attendance.event.pk %}" hx-boost="false">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
||||
<input type="hidden" name="status" value="absent">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
screen to lead to and is omitted, same for the mockup's "Coach mode"
|
||||
promo. "Payments & dues" does lead somewhere (mobile:payments) and
|
||||
carries its "N OPEN" pill only once open_dues_count is actually > 0.
|
||||
"Teams I coach" is new, beyond the mockup -- one row per current-season
|
||||
"Teams I coach/manage" is new, beyond the mockup -- one row per current-season
|
||||
staff assignment, linking straight into Coach mode for that team.
|
||||
The avatar/name/subtitle row lives in header_extra -- merged into the
|
||||
shared navy app-header (base.html) rather than a separately-coloured
|
||||
@@ -68,17 +68,19 @@
|
||||
|
||||
{% if staff_assignments %}
|
||||
<div>
|
||||
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Teams I coach" %}</div>
|
||||
<div class="m-card overflow-hidden">
|
||||
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Teams I coach/manage" %}</div>
|
||||
<div class="m-card-dark overflow-hidden">
|
||||
{% for assignment in staff_assignments %}
|
||||
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
|
||||
<a class="flex items-center gap-3 p-3.5" href="{% url "mobile:coach_today" %}?team={{ assignment.team.pk }}">
|
||||
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-ink font-display text-xs font-extrabold text-ice">{{ assignment.team.short_name|slice:":2" }}</span>
|
||||
{% if not forloop.first %}<div class="h-px bg-white/10"></div>{% endif %}
|
||||
<a class="flex items-center gap-3 p-3.5" href="{% url "mobile:coach_today" %}?team={{ assignment.team.pk }}" hx-boost="false">
|
||||
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-steel text-ice">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="3"/><circle cx="17" cy="9" r="2.6"/><path d="M2.5 20c0-3.3 2.5-5.6 5.5-5.6s5.5 2.3 5.5 5.6"/><path d="M14 15c2.6.3 4.5 2.3 4.5 5"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-[15px] font-semibold text-ink">{{ assignment.team.name }}</div>
|
||||
<div class="text-xs text-muted">{{ assignment.position }}</div>
|
||||
<div class="text-[15px] font-semibold text-white">{{ assignment.team.name }}</div>
|
||||
<div class="text-xs text-on-dark-dim">{{ assignment.position }}</div>
|
||||
</div>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="shrink-0 text-dim" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="shrink-0 text-on-dark-dim" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1348,7 +1348,7 @@ class MeViewTests(TestCase):
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertNotContains(response, "Teams I coach")
|
||||
self.assertNotContains(response, "Teams I coach/manage")
|
||||
|
||||
def test_teams_card_lists_each_current_season_staff_assignment(self):
|
||||
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
|
||||
@@ -1358,7 +1358,7 @@ class MeViewTests(TestCase):
|
||||
|
||||
response = self._get()
|
||||
|
||||
self.assertContains(response, "Teams I coach")
|
||||
self.assertContains(response, "Teams I coach/manage")
|
||||
self.assertContains(response, "U16")
|
||||
self.assertContains(response, "Physio")
|
||||
self.assertContains(response, reverse("mobile:coach_today") + "?team=" + str(team.pk))
|
||||
|
||||
@@ -535,7 +535,7 @@ class MeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"Payments & dues" does lead somewhere -- PaymentsView below -- with its
|
||||
"N OPEN" pill only rendered once there's actually a balance owed.
|
||||
|
||||
"Teams I coach" is new, beyond the mockup: one row per current-season
|
||||
"Teams I coach/manage" is new, beyond the mockup: one row per current-season
|
||||
StaffAssignment self.me holds (team + position/role), each linking
|
||||
straight into Coach mode for that team (mobile:coach_today?team=<pk>,
|
||||
which mobile.coach_mixins.CoachScopeMixin's own ?team= handling already
|
||||
|
||||
Reference in New Issue
Block a user