activity_rows.html
html
| 1 | {# |
| 2 | fragments/activity_rows.html — HTMX fragment: filter tabs + date-grouped event timeline. |
| 3 | |
| 4 | Context: |
| 5 | event_groups : list[{label, events}] — events grouped by calendar date |
| 6 | type_pills : list[{type, count}] — per-type counts for filter tabs |
| 7 | event_type : str — active type filter (or "") |
| 8 | total : int — filtered event count |
| 9 | total_all : int — unfiltered total |
| 10 | page : int |
| 11 | total_pages : int |
| 12 | base_url : str |
| 13 | owner : str |
| 14 | #} |
| 15 | {% from "musehub/macros/pagination.html" import pagination %} |
| 16 | |
| 17 | {# ── Event icon macro ─────────────────────────────────────────────────────── #} |
| 18 | {% macro ev_icon(type) %} |
| 19 | {%- if type == 'commit_pushed' -%} |
| 20 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/></svg> |
| 21 | {%- elif type == 'pr_opened' or type == 'pr_merged' -%} |
| 22 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 9v2a3 3 0 0 0 3 3h7"/><polyline points="15 11 18 14 15 17"/></svg> |
| 23 | {%- elif type == 'pr_closed' -%} |
| 24 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> |
| 25 | {%- elif type == 'issue_opened' -%} |
| 26 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> |
| 27 | {%- elif type == 'issue_closed' -%} |
| 28 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> |
| 29 | {%- elif type == 'branch_created' -%} |
| 30 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> |
| 31 | {%- elif type == 'branch_deleted' -%} |
| 32 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> |
| 33 | {%- elif type == 'tag_pushed' -%} |
| 34 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg> |
| 35 | {%- elif type == 'session_started' or type == 'session_ended' -%} |
| 36 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> |
| 37 | {%- else -%} |
| 38 | <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/></svg> |
| 39 | {%- endif %} |
| 40 | {% endmacro %} |
| 41 | |
| 42 | {# ── Event type config ────────────────────────────────────────────────────── #} |
| 43 | {%- set ev_cfg = { |
| 44 | "commit_pushed": "Commit", |
| 45 | "pr_opened": "PR Opened", |
| 46 | "pr_merged": "PR Merged", |
| 47 | "pr_closed": "PR Closed", |
| 48 | "issue_opened": "Issue", |
| 49 | "issue_closed": "Issue Closed", |
| 50 | "branch_created": "Branch", |
| 51 | "branch_deleted": "Branch Deleted", |
| 52 | "tag_pushed": "Tag", |
| 53 | "session_started": "Session", |
| 54 | "session_ended": "Session Ended", |
| 55 | } %} |
| 56 | |
| 57 | {# ── Filter tabs ──────────────────────────────────────────────────────────── #} |
| 58 | {%- set pill_base = base_url ~ "/activity" %} |
| 59 | <div class="av-filter-bar"> |
| 60 | <a class="av-pill {% if not event_type %}av-pill--active{% endif %}" |
| 61 | href="{{ pill_base }}" |
| 62 | hx-get="{{ pill_base }}" |
| 63 | hx-target="#av-feed" |
| 64 | hx-push-url="true"> |
| 65 | All <span class="av-pill-count">{{ total_all }}</span> |
| 66 | </a> |
| 67 | |
| 68 | {%- for pill in type_pills %} |
| 69 | {%- if pill.count > 0 %} |
| 70 | {%- set label = ev_cfg.get(pill.type, pill.type | replace("_"," ") | title) %} |
| 71 | <a class="av-pill {% if event_type == pill.type %}av-pill--active{% endif %}" |
| 72 | href="{{ pill_base }}?event_type={{ pill.type }}" |
| 73 | hx-get="{{ pill_base }}?event_type={{ pill.type }}" |
| 74 | hx-target="#av-feed" |
| 75 | hx-push-url="true"> |
| 76 | {{ ev_icon(pill.type) }} {{ label }} |
| 77 | <span class="av-pill-count">{{ pill.count }}</span> |
| 78 | </a> |
| 79 | {%- endif %} |
| 80 | {%- endfor %} |
| 81 | |
| 82 | {%- if event_type %} |
| 83 | <span class="av-filter-count">{{ total }} shown</span> |
| 84 | {%- endif %} |
| 85 | </div> |
| 86 | |
| 87 | {# ── Timeline ─────────────────────────────────────────────────────────────── #} |
| 88 | {%- if event_groups %} |
| 89 | |
| 90 | {%- for group in event_groups %} |
| 91 | <div class="av-date-header"> |
| 92 | <span>{{ group.label }}</span> |
| 93 | <div class="av-date-line"></div> |
| 94 | <span>{{ group.events | length }} event{{ '' if group.events | length == 1 else 's' }}</span> |
| 95 | </div> |
| 96 | |
| 97 | {%- for event in group.events %} |
| 98 | {%- set label = ev_cfg.get(event.event_type, event.event_type | replace("_"," ") | title) %} |
| 99 | {%- set meta = event.metadata | default({}) %} |
| 100 | |
| 101 | <div class="av-row" id="event-{{ event.event_id }}"> |
| 102 | <div class="av-row-icon-col"> |
| 103 | <div class="av-icon-badge av-type-{{ event.event_type }}">{{ ev_icon(event.event_type) }}</div> |
| 104 | {%- if not loop.last %} |
| 105 | <div class="av-row-connector"></div> |
| 106 | {%- endif %} |
| 107 | </div> |
| 108 | |
| 109 | <div class="av-row-body"> |
| 110 | <div class="av-row-title">{{ event.description }}</div> |
| 111 | |
| 112 | <div class="av-row-meta"> |
| 113 | <a href="/{{ event.actor }}" class="av-actor-link"> |
| 114 | <span class="av-actor-avatar">{{ event.actor[0] | upper if event.actor else '?' }}</span> |
| 115 | {{ event.actor }} |
| 116 | </a> |
| 117 | |
| 118 | <span class="av-type-label">{{ label }}</span> |
| 119 | |
| 120 | {%- if meta.get("commit_id") %} |
| 121 | <a href="{{ base_url }}/commits/{{ meta.commit_id }}" class="av-chip av-chip-commit">{{ meta.commit_id[:7] }}</a> |
| 122 | {%- endif %} |
| 123 | |
| 124 | {%- if meta.get("branch") %} |
| 125 | <span class="av-chip av-chip-branch"> |
| 126 | <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> |
| 127 | {{ meta.branch }} |
| 128 | </span> |
| 129 | {%- endif %} |
| 130 | |
| 131 | {%- if meta.get("pr_number") %} |
| 132 | <a href="{{ base_url }}/pull-requests" class="av-chip av-chip-pr">#{{ meta.pr_number }}</a> |
| 133 | {%- elif meta.get("pr_id") %} |
| 134 | <span class="av-chip av-chip-pr">PR</span> |
| 135 | {%- endif %} |
| 136 | |
| 137 | {%- if meta.get("tag") %} |
| 138 | <span class="av-chip av-chip-tag"> |
| 139 | <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg> |
| 140 | {{ meta.tag }} |
| 141 | </span> |
| 142 | {%- endif %} |
| 143 | |
| 144 | {%- if meta.get("participants") %} |
| 145 | <span class="av-chip av-chip-session">{{ meta.participants | length }} participant{{ '' if meta.participants | length == 1 else 's' }}</span> |
| 146 | {%- endif %} |
| 147 | |
| 148 | <span data-iso="{{ event.created_at }}">{{ event.created_at | fmtrelative }}</span> |
| 149 | </div> |
| 150 | </div> |
| 151 | </div> |
| 152 | {%- endfor %} |
| 153 | {%- endfor %} |
| 154 | |
| 155 | {%- if total_pages > 1 %} |
| 156 | <div class="av-pagination"> |
| 157 | {{ pagination(page, total_pages, base_url ~ '/activity', |
| 158 | extra_params={'event_type': event_type} if event_type else {}) }} |
| 159 | </div> |
| 160 | {%- endif %} |
| 161 | |
| 162 | {%- else %} |
| 163 | <div class="av-empty"> |
| 164 | {%- if event_type %} |
| 165 | <div class="av-empty-icon"> |
| 166 | <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> |
| 167 | </div> |
| 168 | <div class="av-empty-title">No {{ ev_cfg.get(event_type, event_type) | lower }} events</div> |
| 169 | <div class="av-empty-desc">No events of this type have been recorded yet.</div> |
| 170 | <a href="{{ base_url }}/activity" |
| 171 | hx-get="{{ base_url }}/activity" |
| 172 | hx-target="#av-feed" |
| 173 | hx-push-url="true" |
| 174 | class="av-empty-link">Clear filter →</a> |
| 175 | {%- else %} |
| 176 | <div class="av-empty-icon"> |
| 177 | <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg> |
| 178 | </div> |
| 179 | <div class="av-empty-title">No activity yet</div> |
| 180 | <div class="av-empty-desc">Push a commit to see the feed come alive.</div> |
| 181 | {%- endif %} |
| 182 | </div> |
| 183 | {%- endif %} |