Bigger unread badge with a real count, and a Clear all action for notifications
The header bell's unread indicator was an 8px dot -- now a proper count badge (capped at "9+"), matching the size/legibility of similar badges elsewhere in the app. Notifications also gains "Clear all" next to "Mark all read" -- a hard delete of the list (the same clear-all gesture a phone's own notification centre uses), not another read-state flip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -67,7 +67,11 @@
|
|||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<a class="relative flex h-11 w-11 shrink-0 items-center justify-center" href="{% url "mobile:notifications" %}" aria-label="{% trans "Notifications" %}">
|
<a class="relative flex h-11 w-11 shrink-0 items-center justify-center" href="{% url "mobile:notifications" %}" aria-label="{% trans "Notifications" %}">
|
||||||
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||||
{% if unread_notification_count %}<span class="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-club" style="border: 2px solid var(--color-navy)"></span>{% endif %}
|
{% if unread_notification_count %}
|
||||||
|
<span class="absolute -top-1 -right-1 flex h-[19px] min-w-[19px] items-center justify-center rounded-full bg-club px-1 font-mono text-[11px] font-bold text-white" style="border: 2px solid var(--color-navy)">
|
||||||
|
{% if unread_notification_count > 9 %}9+{% else %}{{ unread_notification_count }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% block header_extra %}{% endblock header_extra %}
|
{% block header_extra %}{% endblock header_extra %}
|
||||||
|
|||||||
@@ -25,12 +25,23 @@
|
|||||||
<button type="button" class="btn btn-dark shrink-0" @click="window.rosterchiefPush.subscribe(); requested = true">{% trans "Enable" %}</button>
|
<button type="button" class="btn btn-dark shrink-0" @click="window.rosterchiefPush.subscribe(); requested = true">{% trans "Enable" %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if today or earlier_this_week or older %}
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
{% if unread_notification_count %}
|
{% if unread_notification_count %}
|
||||||
<form method="post" action="{% url "mobile:notifications" %}">
|
<form method="post" action="{% url "mobile:notifications" %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="mark_all_read">
|
<input type="hidden" name="action" value="mark_all_read">
|
||||||
<button class="font-display text-xs font-extrabold tracking-wide text-club uppercase" type="submit">{% trans "Mark all read" %}</button>
|
<button class="font-display text-xs font-extrabold tracking-wide text-club uppercase" type="submit">{% trans "Mark all read" %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="{% url "mobile:notifications" %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="clear_all">
|
||||||
|
<button class="font-display text-xs font-extrabold tracking-wide text-muted uppercase" type="submit">{% trans "Clear all" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not today and not earlier_this_week and not older %}
|
{% if not today and not earlier_this_week and not older %}
|
||||||
|
|||||||
@@ -910,6 +910,19 @@ class NotificationsViewTests(TestCase):
|
|||||||
follow_up = self._get()
|
follow_up = self._get()
|
||||||
self.assertEqual(follow_up.context["unread_notification_count"], 0)
|
self.assertEqual(follow_up.context["unread_notification_count"], 0)
|
||||||
|
|
||||||
|
def test_clear_all_deletes_every_notification_for_every_managed_person(self):
|
||||||
|
Notification.objects.create(club=self.club, member=self.member, title="First", body="Body.")
|
||||||
|
Notification.objects.create(club=self.club, member=self.child, title="Second", body="Body.")
|
||||||
|
stranger = Member.objects.create(first_name="Someone", last_name="Else")
|
||||||
|
untouched = Notification.objects.create(club=self.club, member=stranger, title="Not yours", body="Body.")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self._post({"action": "clear_all"})
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse("mobile:notifications"), fetch_redirect_response=False)
|
||||||
|
self.assertEqual(Notification.objects.filter(member__in=[self.member, self.child]).count(), 0)
|
||||||
|
self.assertTrue(Notification.objects.filter(pk=untouched.pk).exists())
|
||||||
|
|
||||||
def test_mark_read_marks_a_single_notification_and_redirects_back(self):
|
def test_mark_read_marks_a_single_notification_and_redirects_back(self):
|
||||||
notification = Notification.objects.create(club=self.club, member=self.member, title="First", body="Body.")
|
notification = Notification.objects.create(club=self.club, member=self.member, title="First", body="Body.")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|||||||
@@ -755,6 +755,13 @@ class NotificationsView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
|||||||
Notification.objects.filter(club=request.club, member__in=self.managed_people, read_at__isnull=True).update(read_at=timezone.now())
|
Notification.objects.filter(club=request.club, member__in=self.managed_people, read_at__isnull=True).update(read_at=timezone.now())
|
||||||
return HttpResponseRedirect(reverse("mobile:notifications"))
|
return HttpResponseRedirect(reverse("mobile:notifications"))
|
||||||
|
|
||||||
|
if action == "clear_all":
|
||||||
|
# A hard delete, not another read-state flip -- "Clear" empties the
|
||||||
|
# list itself, same as the clear-all gesture in a phone's own
|
||||||
|
# notification centre, rather than just marking everything read.
|
||||||
|
Notification.objects.filter(club=request.club, member__in=self.managed_people).delete()
|
||||||
|
return HttpResponseRedirect(reverse("mobile:notifications"))
|
||||||
|
|
||||||
if action == "mark_read":
|
if action == "mark_read":
|
||||||
notification = Notification.objects.filter(pk=request.POST.get("notification_id"), club=request.club, member__in=self.managed_people).first()
|
notification = Notification.objects.filter(pk=request.POST.get("notification_id"), club=request.club, member__in=self.managed_people).first()
|
||||||
if notification is None:
|
if notification is None:
|
||||||
|
|||||||
@@ -3944,6 +3944,9 @@
|
|||||||
.h-\[7px\] {
|
.h-\[7px\] {
|
||||||
height: 7px;
|
height: 7px;
|
||||||
}
|
}
|
||||||
|
.h-\[19px\] {
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
.h-\[22px\] {
|
.h-\[22px\] {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
@@ -4169,6 +4172,9 @@
|
|||||||
.min-w-40 {
|
.min-w-40 {
|
||||||
min-width: calc(var(--spacing) * 40);
|
min-width: calc(var(--spacing) * 40);
|
||||||
}
|
}
|
||||||
|
.min-w-\[19px\] {
|
||||||
|
min-width: 19px;
|
||||||
|
}
|
||||||
.min-w-\[220px\] {
|
.min-w-\[220px\] {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user