the home site for me: also iteration 3 or 4 of my site
1{% set api_url =
2"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update"
3%} {% set response = load_data(url=api_url, format="json") %}
4
5<style>
6 #status-updates-container {
7 display: flex;
8 flex-direction: column;
9 gap: 1.5rem;
10 width: 100%;
11 margin-bottom: 2rem;
12 }
13
14 .bsky-post {
15 border-left: 0.375rem solid var(--accent);
16 padding: 0.7em 1em;
17 font-size: 1rem;
18 background-color: var(--bg-light);
19 border-radius: 0.375rem;
20 }
21
22 .bsky-post-content {
23 margin-bottom: 0.75rem;
24 line-height: 1.4;
25 }
26
27 .bsky-post-footer {
28 display: flex;
29 justify-content: space-between;
30 align-items: center;
31 color: var(--text-light);
32 }
33
34 .bsky-post-footer cite {
35 display: inline-flex;
36 align-items: center;
37 gap: 0.4rem;
38 }
39
40 .bsky-post-time {
41 font-size: 0.8rem;
42 color: var(--text-light);
43 }
44</style>
45
46<div id="status-updates-container">
47 {% if response.records %} {% for record in response.records |
48 sort(attribute="value.createdAt") | reverse %} {% set created_at =
49 record.value.createdAt %} {% set status_text = record.value.text %}
50 <div
51 class="bsky-post"
52 data-cid="{{ record.cid }}"
53 data-created="{{ created_at }}"
54 >
55 <div class="bsky-post-content">Kieran was {{ status_text }}</div>
56 <div class="bsky-post-footer">
57 <cite>
58 <img
59 src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg"
60 alt="Kieran's avatar"
61 class="avatar"
62 />
63 <a
64 href="https://bsky.app/profile/doing.dunkirk.sh"
65 target="_blank"
66 rel="noopener"
67 >@doing.dunkirk.sh</a
68 >
69 </cite>
70 <span class="bsky-post-time">
71 {{ record.value.createdAt | date(format="%b %d, %Y") }}
72 </span>
73 </div>
74 </div>
75 {% endfor %} {% else %}
76 <div class="bsky-post">
77 <div class="bsky-post-content">No status updates found.</div>
78 </div>
79 {% endif %}
80</div>
81
82<script>
83 document.addEventListener("DOMContentLoaded", () => {
84 const container = document.getElementById("status-updates-container");
85 const API_URL =
86 "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update";
87 const existingPosts = new Map();
88
89 // Collect existing posts by CID
90 document.querySelectorAll(".bsky-post[data-cid]").forEach((post) => {
91 existingPosts.set(post.dataset.cid, {
92 element: post,
93 created: new Date(post.dataset.created),
94 });
95 });
96
97 // Format time relative to now
98 function formatTimeAgo(date) {
99 const now = new Date();
100 const diffInMs = now - date;
101 const diffInMins = Math.floor(diffInMs / (1000 * 60));
102 const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
103
104 if (diffInMins < 1) return "just now";
105 if (diffInMins < 60) return `${Math.round(diffInMins)}m`;
106 if (diffInHours < 24) return `${Math.round(diffInHours)}h`;
107
108 return new Intl.DateTimeFormat("en", {
109 month: "short",
110 day: "numeric",
111 }).format(date);
112 }
113
114 // Update timestamps and verbs on existing posts
115 function updateTimestamps() {
116 existingPosts.forEach((post) => {
117 const timeElement =
118 post.element.querySelector(".bsky-post-time");
119 const contentElement =
120 post.element.querySelector(".bsky-post-content");
121 if (timeElement) {
122 timeElement.textContent = formatTimeAgo(post.created);
123 }
124
125 // Update the is/was verb based on post age
126 const now = new Date();
127 const diffInMs = now - post.created;
128 const diffInMins = diffInMs / (1000 * 60);
129 const verb = diffInMins < 30 ? "is" : "was";
130
131 // Get the status text (everything after "Kieran was/is ")
132 if (contentElement) {
133 const text = contentElement.textContent;
134 const statusText = text.replace(/^Kieran (is|was) /, "");
135 contentElement.textContent = `Kieran ${verb} ${statusText}`;
136 }
137 });
138 }
139
140 // Create a new post element
141 function createPostElement(record) {
142 const createdDate = new Date(record.value.createdAt);
143 const postElement = document.createElement("div");
144 postElement.className = "bsky-post";
145 postElement.dataset.cid = record.cid;
146 postElement.dataset.created = record.value.createdAt;
147
148 // Determine if status is recent (within 30 minutes)
149 const now = new Date();
150 const diffInMs = now - createdDate;
151 const diffInMins = diffInMs / (1000 * 60);
152 const verb = diffInMins < 30 ? "is" : "was";
153
154 postElement.innerHTML = `
155 <div class="bsky-post-content">Kieran ${verb} ${record.value.text}</div>
156 <div class="bsky-post-footer">
157 <cite>
158 <img src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg" alt="Kieran's avatar" class="avatar" />
159 <a href="https://bsky.app/@doing.dunkirk.sh" target="_blank" rel="noopener">@doing.dunkirk.sh</a>
160 </cite>
161 <span class="bsky-post-time">${formatTimeAgo(createdDate)}</span>
162 </div>
163 `;
164
165 return postElement;
166 }
167
168 // Fetch and update posts
169 function fetchAndUpdatePosts() {
170 fetch(API_URL)
171 .then((response) => response.json())
172 .then((data) => {
173 if (!data.records || data.records.length === 0) {
174 if (existingPosts.size === 0) {
175 container.innerHTML =
176 '<div class="bsky-post"><div class="bsky-post-content">No status updates found.</div></div>';
177 }
178 return;
179 }
180
181 // Sort newest first
182 const sortedRecords = data.records.sort((a, b) => {
183 return (
184 new Date(b.value.createdAt) -
185 new Date(a.value.createdAt)
186 );
187 });
188
189 // Track if we need to reorder
190 let needsReordering = false;
191
192 // Add new posts
193 for (const record of sortedRecords) {
194 if (!existingPosts.has(record.cid)) {
195 const newPostElement = createPostElement(record);
196 // Always insert at the beginning for now (we'll reorder if needed)
197 container.insertBefore(
198 newPostElement,
199 container.firstChild,
200 );
201 existingPosts.set(record.cid, {
202 element: newPostElement,
203 created: new Date(record.value.createdAt),
204 });
205 needsReordering = true;
206 }
207 }
208
209 // If we added new posts, reorder everything
210 if (needsReordering) {
211 const sortedElements = [...existingPosts.entries()]
212 .sort((a, b) => b[1].created - a[1].created)
213 .map((entry) => entry[1].element);
214
215 // Reattach in correct order
216 sortedElements.forEach((element) => {
217 container.appendChild(element);
218 });
219 }
220
221 // Update all timestamps
222 updateTimestamps();
223 })
224 .catch((error) => {
225 console.error("Error fetching status updates:", error);
226 });
227 }
228
229 // Initial update
230 fetchAndUpdatePosts();
231
232 // Update timestamps every minute
233 setInterval(updateTimestamps, 60000);
234
235 // Fetch new posts every 5 minutes
236 setInterval(fetchAndUpdatePosts, 300000);
237 });
238</script>