From c08dd145c33f2caa5b7b2f10da041a60a775033d Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 12 May 2026 23:06:55 -0400 Subject: [PATCH] =?UTF-8?q?applet=20rows:=203-col=20grid=20`=20|=20?= =?UTF-8?q?<body>=20|=20<ts>`=20mirroring=20post.html's=20`.post-line`=20s?= =?UTF-8?q?hape=20=E2=80=94=20`=5Fmy=5Fposts=5Fapplet=5Fitem=20/=20=5Fmy?= =?UTF-8?q?=5Fbuds=5Fapplet=5Fitem=20/=20=5Fmy=5Fnotes=5Fitem=20/=20=5Fmy?= =?UTF-8?q?=5Fscrolls=5Fitem=20/=20=5Fmy=5Fgames=5Fitem`=20all=20gain=20a?= =?UTF-8?q?=20`.applet-list-entry.row-3col`=20w.=20`<a=20class=3D"row-titl?= =?UTF-8?q?e">`=20(clickable,=2035c/32+...=20server-side=20truncated=20via?= =?UTF-8?q?=20new=20`lyric=5Fextras.truncate=5Ftitle`=20filter)=20+=20`<sp?= =?UTF-8?q?an=20class=3D"row-body">`=20(most-recent=20activity=20excerpt,?= =?UTF-8?q?=20dimmed=200.6=20opacity,=20CSS-`text-overflow:=20ellipsis`=20?= =?UTF-8?q?clipped=20to=20whatever=20space=20remains=20=E2=80=94=20no=20se?= =?UTF-8?q?rver-side=20trunc=20here=20so=20the=20full=20line=20lives=20in?= =?UTF-8?q?=20the=20DOM=20for=20inspectors)=20+=20`<time=20class=3D"row-ts?= =?UTF-8?q?">`=20(`relative=5Fts`=20formatted,=20same=20`minmax(3rem,auto)?= =?UTF-8?q?`=20rightward=20column=20allocation=20post.html's=20`.post-line?= =?UTF-8?q?-time`=20uses,=20font-size=200.75rem=20+=20opacity=200.5=20+=20?= =?UTF-8?q?right-aligned=20+=20nowrap);=20SCSS=20grid=20`minmax(4rem,auto)?= =?UTF-8?q?=201fr=20minmax(3rem,auto)`=20lifted=20from=20`.post-line`'s=20?= =?UTF-8?q?template=20so=20the=20timestamp=20column=20lines=20up=20across?= =?UTF-8?q?=20post.html=20/=20scroll.html=20/=20every=20applet=20list;=20p?= =?UTF-8?q?er-applet=20data=20shapes=20=E2=80=94=20`=5Frecent=5Fposts`=20a?= =?UTF-8?q?nnotates=20each=20Post=20w.=20`latest=5Fline`=20(Line=20FK=20or?= =?UTF-8?q?dered=20by=20-id,=20None=20for=20empty=20Note-unlock=20posts);?= =?UTF-8?q?=20`=5Frecent=5Fbuds`=20`select=5Frelated('to=5Fuser=5F=5Factiv?= =?UTF-8?q?e=5Ftitle')`=20warms=20the=20bud's=20donned-Note=20FK=20in=20on?= =?UTF-8?q?e=20query=20for=20the=20buds=20row=20body=20("the=20{{=20bud.ac?= =?UTF-8?q?tive=5Ftitle=5Fdisplay=20}}"=20+=20"since=20{{=20bud.active=5Ft?= =?UTF-8?q?itle.earned=5Fat|relative=5Fts=20}}"=20=E2=80=94=20the=20"since?= =?UTF-8?q?=20"=20prefix=20is=20unique=20to=20this=20row=20since=20the=20t?= =?UTF-8?q?s=20is=20"when=20they=20donned=20it",=20not=20the=20row's=20own?= =?UTF-8?q?=20creation);=20`=5Frecent=5Fnotes`=20attaches=20`description`?= =?UTF-8?q?=20from=20`=5FNOTE=5FMETA`=20per=20slug;=20`annotate=5Flatest?= =?UTF-8?q?=5Fevent(rooms)`=20helper=20added=20to=20`apps.epic.utils`=20(n?= =?UTF-8?q?ext=20to=20`rooms=5Ffor=5Fuser`)=20=E2=80=94=20attaches=20`room?= =?UTF-8?q?.latest=5Fevent`=20per=20Room=20w.=20one=20`.events.order=5Fby(?= =?UTF-8?q?'-timestamp').first()`=20per=20item,=20used=20by=20`=5Fbillboar?= =?UTF-8?q?d=5Fcontext`=20for=20`my=5Frooms`=20(My=20Scrolls=20applet)=20A?= =?UTF-8?q?ND=20by=20`apps.gameboard.views.gameboard`=20+=20`toggle=5Fgame?= =?UTF-8?q?=5Fapplets`=20for=20`my=5Fgames`=20(My=20Games=20applet),=20kee?= =?UTF-8?q?ping=20the=20My=20Scrolls=20+=20My=20Games=20shapes=20symmetric?= =?UTF-8?q?;=20`=5Fbillboard=5Fcontext.my=5Frooms=20=3D=20annotate=5Flates?= =?UTF-8?q?t=5Fevent(...)`=20swaps=20`rooms=5Ffor=5Fuser(...).order=5Fby("?= =?UTF-8?q?-created=5Fat")`=20materialisation=20point=20=E2=80=94=20bud=20?= =?UTF-8?q?row's=20"no=20active=20title"=20branch=20silently=20drops=20bod?= =?UTF-8?q?y=20+=20ts=20cells=20so=20unrecognised=20buds=20still=20surface?= =?UTF-8?q?=20but=20don't=20fabricate=20a=20"since=20None"=20line;=20new?= =?UTF-8?q?=20`truncate=5Ftitle`=20filter=20is=20the=20existing=20`=5Ftrun?= =?UTF-8?q?cate=5Fpost=5Ftitle`=20view=20helper=20hoisted=20into=20the=20t?= =?UTF-8?q?emplate=20namespace=20(literal=20`...`=20past=2035=20chars,=20N?= =?UTF-8?q?one-safe);=205=20ITs=20in=20BillboardViewTest=20cover=20row=20c?= =?UTF-8?q?ontent=20/=20row=20absence=20on=20missing=20activity=20/=20"sin?= =?UTF-8?q?ce"=20prefix=20uniquely=20on=20the=20buds=20row=20+=201=20in=20?= =?UTF-8?q?GameboardViewTest=20for=20My=20Games=20row=20event=20prose;=20d?= =?UTF-8?q?eferred=20row-prose=20body=20content=20cap=20on=20`<span=20clas?= =?UTF-8?q?s=3D"row-body">`=20purely=20to=20CSS=20`text-overflow:=20ellips?= =?UTF-8?q?is`=20per=20user's=20"middle=20col=20should=20take=20up=20the?= =?UTF-8?q?=20remaining=20space"=20steer=20(initial=20pass=20also=20server?= =?UTF-8?q?-side=20trunc'd=20the=20body=20to=2035c;=20removed)=20=E2=80=94?= =?UTF-8?q?=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --- .../billboard/tests/integrated/test_views.py | 78 +++++++++++++++++++ src/apps/billboard/views.py | 26 +++++-- src/apps/epic/utils.py | 11 +++ .../gameboard/tests/integrated/test_views.py | 19 +++++ src/apps/gameboard/views.py | 6 +- src/apps/lyric/templatetags/lyric_extras.py | 14 ++++ src/static_src/scss/_billboard.scss | 33 ++++++++ .../_partials/_my_buds_applet_item.html | 17 ++-- .../billboard/_partials/_my_notes_item.html | 13 +++- .../_partials/_my_posts_applet_item.html | 12 ++- .../billboard/_partials/_my_scrolls_item.html | 12 ++- .../gameboard/_partials/_my_games_item.html | 12 ++- 12 files changed, 227 insertions(+), 26 deletions(-) diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 8d12776..89a56c9 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -152,6 +152,84 @@ class BillboardViewTest(TestCase): response = self.client.get("/billboard/") self.assertEqual(list(response.context["recent_notes"]), []) + # ── 3-column applet row contract ────────────────────────────────── + # Each applet's list item renders `<title> | <body> | <ts>` — + # title + body wrapped in `.row-title` / `.row-body`, timestamp + # in a `<time class="row-ts">` element. The body shows the + # most-recent activity (post line / room event / note description / + # bud's active title). Right column carries `relative_ts` text. + + def test_my_posts_applet_row_shows_latest_line_and_ts(self): + from apps.billboard.models import Line, Post + post = Post.objects.create( + owner=self.user, kind=Post.KIND_USER_POST, title="StampedPost", + ) + Line.objects.create(post=post, text="first line text", author=self.user) + Line.objects.create(post=post, text="latest line text", author=self.user) + response = self.client.get("/billboard/") + body = response.content.decode() + # Title preserved + self.assertIn("StampedPost", body) + # Latest line surfaces in the row body, not the older line + self.assertIn("latest line text", body) + # A <time> ts element renders on the row + self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + + def test_my_buds_applet_row_shows_active_title_and_since_ts(self): + bud = User.objects.create(email="alice@test.io", username="alice") + note = Note.objects.create( + user=bud, slug="stargazer", earned_at=timezone.now(), + ) + bud.active_title = note + bud.save() + self.user.buds.add(bud) + response = self.client.get("/billboard/") + body = response.content.decode() + # Bud handle shows; the active title surfaces as the row body; + # the ts column carries a "since " prefix unique to buds + self.assertIn("alice", body) + self.assertIn("the Stargazer", body) + self.assertRegex(body, r'class="[^"]*row-ts[^"]*"[^>]*>\s*since\s+') + + def test_my_buds_applet_row_no_active_title_no_body_or_ts(self): + bud = User.objects.create(email="alice@test.io", username="alice") + # No active_title donned + self.user.buds.add(bud) + response = self.client.get("/billboard/") + body = response.content.decode() + self.assertIn("alice", body) + # Without an active title we don't render a "since" line + self.assertNotRegex(body, r'class="[^"]*row-ts[^"]*"[^>]*>\s*since\s+') + + def test_my_notes_applet_row_shows_description_and_earned_at(self): + Note.objects.create( + user=self.user, slug="stargazer", earned_at=timezone.now(), + ) + response = self.client.get("/billboard/") + body = response.content.decode() + self.assertIn("Stargazer", body) + # Description prefix from _NOTE_META — full string is 40 chars and + # truncates to 32+"..." via the `truncate_title` filter, so assert + # against the head only ("You saved your first personal" is 30 chars, + # comfortably inside the truncation window). + self.assertIn("You saved your first personal", body) + # ts column present + self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + + def test_my_scrolls_applet_row_shows_latest_event_and_ts(self): + room = Room.objects.create(name="StampedRoom", owner=self.user) + record( + room, GameEvent.SLOT_FILLED, actor=self.user, + slot_number=1, token_type="coin", + token_display="Coin-on-a-String", renewal_days=7, + ) + response = self.client.get("/billboard/") + body = response.content.decode() + self.assertIn("StampedRoom", body) + # The slot-fill prose mentions the token; we just check the room's + # scroll row carries a <time> + some body content (event prose) + self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + class SaveScrollPositionViewTest(TestCase): def setUp(self): diff --git a/src/apps/billboard/views.py b/src/apps/billboard/views.py index 8d3ed14..3c3a11a 100644 --- a/src/apps/billboard/views.py +++ b/src/apps/billboard/views.py @@ -14,14 +14,18 @@ from apps.billboard.models import Brief, Line, Post from apps.dashboard.views import _PALETTE_DEFS from apps.drama.models import GameEvent, Note, ScrollPosition from apps.epic.models import Room -from apps.epic.utils import rooms_for_user +from apps.epic.utils import annotate_latest_event, rooms_for_user from apps.lyric.models import User, get_or_create_adman _PALETTE_LABELS = {p["name"]: p["label"] for p in _PALETTE_DEFS} def _recent_posts(user, limit=3): - return ( + """Most-recently-active Posts the user owns or is shared on. Attaches + `latest_line` to each Post (Line or None) so the My Posts applet row + can render its 3-col `<title> | <latest line text> | <ts>` shape + without an extra template-side query.""" + posts = list( Post .objects .filter(Q(owner=user) | Q(shared_with=user)) @@ -29,27 +33,37 @@ def _recent_posts(user, limit=3): .order_by('-last_line') .distinct()[:limit] ) + for p in posts: + p.latest_line = p.lines.order_by("-id").first() + return posts def _recent_buds(user, limit=3): """Most-recently-added buds, newest first. M2M has no explicit through - model, so we sort the auto-through table by its monotonic `id`.""" + model, so we sort the auto-through table by its monotonic `id`. The + `select_related('to_user__active_title')` chain warms up the active + title FK for the My Buds applet row's middle column.""" through = User.buds.through # type: ignore[attr-defined] rows = ( through.objects .filter(from_user=user) - .select_related("to_user") + .select_related("to_user__active_title") .order_by("-id")[:limit] ) return [r.to_user for r in rows] def _recent_notes(user, limit=3): - return user.notes.order_by("-earned_at")[:limit] + """Most-recently-earned Notes. Attaches `description` from _NOTE_META + so the My Notes applet row can render `<title> | <description> | <ts>`.""" + notes = list(user.notes.order_by("-earned_at")[:limit]) + for n in notes: + n.description = _NOTE_META.get(n.slug, {}).get("description", "") + return notes def _billboard_context(user): - my_rooms = rooms_for_user(user).order_by("-created_at") + my_rooms = annotate_latest_event(rooms_for_user(user).order_by("-created_at")) recent_room = ( Room.objects.filter( diff --git a/src/apps/epic/utils.py b/src/apps/epic/utils.py index f2255d4..aa92979 100644 --- a/src/apps/epic/utils.py +++ b/src/apps/epic/utils.py @@ -98,3 +98,14 @@ def rooms_for_user(user): Q(gate_slots__gamer=user) | Q(invites__invitee_email=user.email, invites__status=RoomInvite.PENDING) ).distinct() + + +def annotate_latest_event(rooms): + """Attach `latest_event` (GameEvent or None) to each Room in the iterable. + Materialises the queryset to a list. Shared by the My Scrolls (billboard) + + My Games (gameboard) applet rows so they render the same 3-col + `<title> | <latest event prose> | <ts>` shape from one helper.""" + rooms = list(rooms) + for r in rooms: + r.latest_event = r.events.order_by("-timestamp").first() + return rooms diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index d2a6b1a..12fef24 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -45,6 +45,25 @@ class GameboardViewTest(TestCase): game_items = self.parsed.cssselect("#id_applet_my_games .game-item") self.assertEqual(len(game_items), 0) + def test_my_games_row_shows_latest_event_prose_and_ts(self): + from apps.drama.models import GameEvent, record + room = Room.objects.create(name="StampedRoom", owner=self.user) + record( + room, GameEvent.SLOT_FILLED, actor=self.user, + slot_number=1, token_type="coin", + token_display="Coin-on-a-String", renewal_days=7, + ) + response = self.client.get("/gameboard/") + body = response.content.decode() + self.assertIn("StampedRoom", body) + # Row carries a ts cell from the recorded event + self.assertRegex( + body, + r'#id_applet_my_games|class="[^"]*row-ts'.replace("#", ""), + ) + # A .row-body cell carries some event prose + self.assertRegex(body, r'<time[^>]+class="[^"]*row-ts') + def test_game_kit_has_coin_on_a_string(self): [_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string") diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 31a0218..5e7e718 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -18,7 +18,7 @@ def _annotate_deck_in_use(decks, user): deck.in_use_room_name = active.get(deck.pk) return decks from apps.epic.models import DeckVariant, Room, TableSeat -from apps.epic.utils import rooms_for_user +from apps.epic.utils import annotate_latest_event, rooms_for_user from apps.lyric.models import Token @@ -49,7 +49,7 @@ def gameboard(request): "free_count": len(free_tokens), "applets": applet_context(request.user, "gameboard"), "page_class": "page-gameboard", - "my_games": rooms_for_user(request.user), + "my_games": annotate_latest_event(rooms_for_user(request.user)), } ) @@ -71,7 +71,7 @@ def toggle_game_applets(request): "deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user), "free_tokens": free_tokens, "free_count": len(free_tokens), - "my_games": rooms_for_user(request.user), + "my_games": annotate_latest_event(rooms_for_user(request.user)), }) return redirect("gameboard") diff --git a/src/apps/lyric/templatetags/lyric_extras.py b/src/apps/lyric/templatetags/lyric_extras.py index d37e0f2..2f6f836 100644 --- a/src/apps/lyric/templatetags/lyric_extras.py +++ b/src/apps/lyric/templatetags/lyric_extras.py @@ -54,6 +54,20 @@ def display_name(user): return truncate_email(user.email) +@register.filter +def truncate_title(text, length=35): + """`<length>` chars verbatim, or `<length-3>` chars + literal `...` + past that — mirrors `billboard.views._truncate_post_title` so applet + list rows + new-Post titling share one truncation rule. Falls back + to empty string on None for guard-free template use.""" + if text is None: + return "" + text = str(text) + if len(text) <= length: + return text + return text[: length - 3] + "..." + + @register.filter def at_handle(user): """`@username` when the user has set one; falls back to the truncated diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index f2d3058..dea9a0f 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -37,6 +37,39 @@ margin: 0; } +// 3-col applet row — `<title> | <body> | <ts>`. Mirrors post.html's +// `.post-line` grid (`minmax(4rem,auto) 1fr minmax(3rem,auto)`) so the +// rightward ts column lines up across post.html, scroll.html, and every +// applet list. Title gets the project-wide 35char/32+... truncation via +// the `truncate_title` template filter; body is dimmed to 0.6. +.applet-list-entry.row-3col { + display: grid; + grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto); + align-items: baseline; + gap: 0.5rem; + + .row-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .row-body { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.6; + } + + .row-ts { + font-size: 0.75rem; + opacity: 0.5; + text-align: right; + white-space: nowrap; + } +} + .applet-list-entry { padding: 0.4rem 0; diff --git a/src/templates/apps/billboard/_partials/_my_buds_applet_item.html b/src/templates/apps/billboard/_partials/_my_buds_applet_item.html index 35718ee..c1183fa 100644 --- a/src/templates/apps/billboard/_partials/_my_buds_applet_item.html +++ b/src/templates/apps/billboard/_partials/_my_buds_applet_item.html @@ -1,7 +1,14 @@ {% load lyric_extras %} -{# Recent-bud row for the My Buds in-grid applet. `item` is a User. #} -{# Mirrors the dedicated billbuds page item: just a name chip, no link #} -{# (per-bud surface doesn't exist yet — coming in the 3-col sprint). #} -<li class="applet-list-entry bud-entry" data-bud-id="{{ item.id }}"> - <span class="bud-name">{{ item|display_name }}</span> +{# My Buds applet row — bud handle | the <active title> | since <ts>. #} +{# `item` is a User w. `active_title` select_related'd. Mirrors the #} +{# dedicated billbuds page item shape (.bud-entry / .bud-name) so the #} +{# duplicate-flash add-flow CSS keeps working in this surface too. #} +<li class="applet-list-entry bud-entry row-3col" data-bud-id="{{ item.id }}"> + {# No per-bud landing page yet — the handle is a plain span until #} + {# the 3-col bud surface ships (next sprint). #} + <span class="row-title bud-name">{{ item|at_handle }}</span> + {% if item.active_title %} + <span class="row-body">the {{ item.active_title_display }}</span> + <time class="row-ts" datetime="{{ item.active_title.earned_at|date:'c' }}">since {{ item.active_title.earned_at|relative_ts }}</time> + {% endif %} </li> diff --git a/src/templates/apps/billboard/_partials/_my_notes_item.html b/src/templates/apps/billboard/_partials/_my_notes_item.html index e005e68..592cf92 100644 --- a/src/templates/apps/billboard/_partials/_my_notes_item.html +++ b/src/templates/apps/billboard/_partials/_my_notes_item.html @@ -1,4 +1,11 @@ -{# Recent-note row for the My Notes in-grid applet. `item` is a Note. #} -<li class="applet-list-entry"> - <a href="{% url 'billboard:my_notes' %}">{{ item.display_name }}</a> +{% load lyric_extras %} +{# My Notes applet row — note title | description | earned_at. #} +{# `item` is a Note w. `description` attached by `_recent_notes` (lookup #} +{# into apps.billboard.views._NOTE_META). #} +<li class="applet-list-entry row-3col"> + <a href="{% url 'billboard:my_notes' %}" class="row-title">{{ item.display_name|truncate_title }}</a> + {% if item.description %} + <span class="row-body">{{ item.description|striptags }}</span> + {% endif %} + <time class="row-ts" datetime="{{ item.earned_at|date:'c' }}">{{ item.earned_at|relative_ts }}</time> </li> diff --git a/src/templates/apps/billboard/_partials/_my_posts_applet_item.html b/src/templates/apps/billboard/_partials/_my_posts_applet_item.html index 89326ef..e8d89a9 100644 --- a/src/templates/apps/billboard/_partials/_my_posts_applet_item.html +++ b/src/templates/apps/billboard/_partials/_my_posts_applet_item.html @@ -1,4 +1,10 @@ -{# Recent-posts row for the My Posts in-grid applet. `item` is a Post. #} -<li class="applet-list-entry"> - <a href="{{ item.get_absolute_url }}">{{ item.title }}</a> +{% load lyric_extras %} +{# My Posts applet row — title | latest line text | latest line ts. #} +{# `item` is a Post w. `latest_line` attached by `_recent_posts`. #} +<li class="applet-list-entry row-3col"> + <a href="{{ item.get_absolute_url }}" class="row-title">{{ item.title|truncate_title }}</a> + {% if item.latest_line %} + <span class="row-body">{{ item.latest_line.text|striptags }}</span> + <time class="row-ts" datetime="{{ item.latest_line.created_at|date:'c' }}">{{ item.latest_line.created_at|relative_ts }}</time> + {% endif %} </li> diff --git a/src/templates/apps/billboard/_partials/_my_scrolls_item.html b/src/templates/apps/billboard/_partials/_my_scrolls_item.html index c870ee5..538e324 100644 --- a/src/templates/apps/billboard/_partials/_my_scrolls_item.html +++ b/src/templates/apps/billboard/_partials/_my_scrolls_item.html @@ -1,4 +1,10 @@ -{# Recent-scroll row for the My Scrolls in-grid applet. `item` is a Room. #} -<li class="applet-list-entry"> - <a href="{% url 'billboard:scroll' item.id %}">{{ item.name }}</a> +{% load lyric_extras %} +{# My Scrolls applet row — room name | latest event prose | event ts. #} +{# `item` is a Room w. `latest_event` attached by `annotate_latest_event`. #} +<li class="applet-list-entry row-3col"> + <a href="{% url 'billboard:scroll' item.id %}" class="row-title">{{ item.name|truncate_title }}</a> + {% if item.latest_event %} + <span class="row-body">{{ item.latest_event.to_prose|striptags }}</span> + <time class="row-ts" datetime="{{ item.latest_event.timestamp|date:'c' }}">{{ item.latest_event.timestamp|relative_ts }}</time> + {% endif %} </li> diff --git a/src/templates/apps/gameboard/_partials/_my_games_item.html b/src/templates/apps/gameboard/_partials/_my_games_item.html index d8db5a4..4838fff 100644 --- a/src/templates/apps/gameboard/_partials/_my_games_item.html +++ b/src/templates/apps/gameboard/_partials/_my_games_item.html @@ -1,4 +1,10 @@ -{# Recent-game row for the My Games in-grid applet. `item` is a Room. #} -<li class="applet-list-entry"> - <a href="{% url 'epic:gatekeeper' item.id %}">{{ item.name }}</a> +{% load lyric_extras %} +{# My Games applet row — room name | latest event prose | event ts. #} +{# `item` is a Room w. `latest_event` attached by `annotate_latest_event`. #} +<li class="applet-list-entry row-3col"> + <a href="{% url 'epic:gatekeeper' item.id %}" class="row-title">{{ item.name|truncate_title }}</a> + {% if item.latest_event %} + <span class="row-body">{{ item.latest_event.to_prose|striptags }}</span> + <time class="row-ts" datetime="{{ item.latest_event.timestamp|date:'c' }}">{{ item.latest_event.timestamp|relative_ts }}</time> + {% endif %} </li>