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
|
account with >=1 current-season staff assignment -- mobile/mixins.py's
|
||||||
PersonScopeMixin) -- Coach mode (C1-C6, mobile/coach_views.py) is being
|
PersonScopeMixin) -- Coach mode (C1-C6, mobile/coach_views.py) is being
|
||||||
built out screen by screen; only what's actually shipped is linked to.
|
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 %}
|
{% endcomment %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
||||||
@@ -51,7 +62,7 @@
|
|||||||
{% block extra_head %}{% endblock extra_head %}
|
{% block extra_head %}{% endblock extra_head %}
|
||||||
</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">
|
<header class="app-header">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<a class="flex items-center gap-2.5" href="{% url "mobile:home" %}">
|
<a class="flex items-center gap-2.5" href="{% url "mobile:home" %}">
|
||||||
@@ -78,7 +89,7 @@
|
|||||||
{% if has_coach_access %}
|
{% if has_coach_access %}
|
||||||
<div class="role-switcher">
|
<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 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block header_extra %}{% endblock header_extra %}
|
{% 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>
|
<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">
|
<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>
|
<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 }">
|
<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
|
mobile:icon/mobile:service_worker PWA plumbing as Member mode -- one app, one
|
||||||
manifest, two modes, not two separate PWAs.
|
manifest, two modes, not two separate PWAs.
|
||||||
|
|
||||||
The tab bar only ever links to screens that actually exist. Today (C1) is the only
|
The tab bar deliberately stays minimal (Today + Me) even with all six coach screens
|
||||||
coach screen built so far, so it's the only coach item -- Me reuses the *existing*
|
built -- C2/C3/C4/C5/C6 are reached from Today's own action buttons and "needs you"
|
||||||
member mobile:me page rather than a separate coach-Me screen (see CoachTodayView's
|
list, not separate tab items; Me reuses the *existing* member mobile:me page rather
|
||||||
own docstring). No dead links: as C2/C4/C5/C6 land, they get their own tab/entry
|
than a separate coach-Me screen (see CoachTodayView's own docstring).
|
||||||
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
|
hx-boost="true" on <body>: every same-shell link/form becomes an AJAX navigation
|
||||||
this existed).
|
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 %}
|
{% endcomment %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
<html lang="{{ LANGUAGE_CODE|default:"en" }}">
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
{% block extra_head %}{% endblock extra_head %}
|
{% block extra_head %}{% endblock extra_head %}
|
||||||
</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">
|
<header class="coach-header">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
{% if club.logo %}
|
{% if club.logo %}
|
||||||
@@ -79,8 +82,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="role-switcher">
|
<div class="role-switcher">
|
||||||
<a class="role-switcher-item" href="{% url "mobile:home" %}">{% trans "Member" %}</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 "Coach" %}</a>
|
<a class="role-switcher-item role-switcher-item-active" href="{% url "mobile:coach_today" %}">{% trans "Manager" %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block header_extra %}{% endblock header_extra %}
|
{% block header_extra %}{% endblock header_extra %}
|
||||||
|
|||||||
@@ -86,14 +86,21 @@
|
|||||||
{% if rsvp_closed %}
|
{% if rsvp_closed %}
|
||||||
<span class="pill pill-neutral mt-3">{{ hero_attendance.get_status_display }}</span>
|
<span class="pill pill-neutral mt-3">{{ hero_attendance.get_status_display }}</span>
|
||||||
{% else %}
|
{% 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">
|
<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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
||||||
<input type="hidden" name="status" value="present">
|
<input type="hidden" name="status" value="present">
|
||||||
<button class="btn btn-positive w-full" type="submit">{% trans "In" %}</button>
|
<button class="btn btn-positive w-full" type="submit">{% trans "In" %}</button>
|
||||||
</form>
|
</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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
<input type="hidden" name="member_id" value="{{ hero_attendance.member.pk }}">
|
||||||
<input type="hidden" name="status" value="absent">
|
<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"
|
screen to lead to and is omitted, same for the mockup's "Coach mode"
|
||||||
promo. "Payments & dues" does lead somewhere (mobile:payments) and
|
promo. "Payments & dues" does lead somewhere (mobile:payments) and
|
||||||
carries its "N OPEN" pill only once open_dues_count is actually > 0.
|
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.
|
staff assignment, linking straight into Coach mode for that team.
|
||||||
The avatar/name/subtitle row lives in header_extra -- merged into the
|
The avatar/name/subtitle row lives in header_extra -- merged into the
|
||||||
shared navy app-header (base.html) rather than a separately-coloured
|
shared navy app-header (base.html) rather than a separately-coloured
|
||||||
@@ -68,17 +68,19 @@
|
|||||||
|
|
||||||
{% if staff_assignments %}
|
{% if staff_assignments %}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Teams I coach" %}</div>
|
<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 overflow-hidden">
|
<div class="m-card-dark overflow-hidden">
|
||||||
{% for assignment in staff_assignments %}
|
{% for assignment in staff_assignments %}
|
||||||
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
|
{% 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 }}">
|
<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-ink font-display text-xs font-extrabold text-ice">{{ assignment.team.short_name|slice:":2" }}</span>
|
<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="min-w-0 flex-1">
|
||||||
<div class="text-[15px] font-semibold text-ink">{{ assignment.team.name }}</div>
|
<div class="text-[15px] font-semibold text-white">{{ assignment.team.name }}</div>
|
||||||
<div class="text-xs text-muted">{{ assignment.position }}</div>
|
<div class="text-xs text-on-dark-dim">{{ assignment.position }}</div>
|
||||||
</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>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1348,7 +1348,7 @@ class MeViewTests(TestCase):
|
|||||||
|
|
||||||
response = self._get()
|
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):
|
def test_teams_card_lists_each_current_season_staff_assignment(self):
|
||||||
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
|
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
|
||||||
@@ -1358,7 +1358,7 @@ class MeViewTests(TestCase):
|
|||||||
|
|
||||||
response = self._get()
|
response = self._get()
|
||||||
|
|
||||||
self.assertContains(response, "Teams I coach")
|
self.assertContains(response, "Teams I coach/manage")
|
||||||
self.assertContains(response, "U16")
|
self.assertContains(response, "U16")
|
||||||
self.assertContains(response, "Physio")
|
self.assertContains(response, "Physio")
|
||||||
self.assertContains(response, reverse("mobile:coach_today") + "?team=" + str(team.pk))
|
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
|
"Payments & dues" does lead somewhere -- PaymentsView below -- with its
|
||||||
"N OPEN" pill only rendered once there's actually a balance owed.
|
"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
|
StaffAssignment self.me holds (team + position/role), each linking
|
||||||
straight into Coach mode for that team (mobile:coach_today?team=<pk>,
|
straight into Coach mode for that team (mobile:coach_today?team=<pk>,
|
||||||
which mobile.coach_mixins.CoachScopeMixin's own ?team= handling already
|
which mobile.coach_mixins.CoachScopeMixin's own ?team= handling already
|
||||||
|
|||||||
@@ -4181,6 +4181,9 @@
|
|||||||
.min-w-4 {
|
.min-w-4 {
|
||||||
min-width: calc(var(--spacing) * 4);
|
min-width: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.min-w-10 {
|
||||||
|
min-width: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
.min-w-40 {
|
.min-w-40 {
|
||||||
min-width: calc(var(--spacing) * 40);
|
min-width: calc(var(--spacing) * 40);
|
||||||
}
|
}
|
||||||
@@ -4603,6 +4606,12 @@
|
|||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
.bg-white\/10 {
|
||||||
|
background-color: color-mix(in srgb, #fff 10%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.bg-white\/15 {
|
.bg-white\/15 {
|
||||||
background-color: color-mix(in srgb, #fff 15%, transparent);
|
background-color: color-mix(in srgb, #fff 15%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user