gabriel / musehub public
activity_rows.html html
183 lines 10.0 KB
9236d457 fix: remove all inline scripts, restore strict script-src CSP (#51) Gabriel Cardona <cgcardona@gmail.com> 23h ago
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 %}