···
1
+
{% set api_url = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update&limit=50" %}
2
+
{% set response = load_data(url=api_url, format="json") %}
5
+
#status-updates-container {
7
+
flex-direction: column;
10
+
margin-bottom: 2rem;
14
+
border-left: 0.375rem solid var(--accent);
17
+
background-color: var(--bg-light);
18
+
border-radius: 0.375rem;
21
+
.bsky-post-content {
22
+
margin-bottom: 0.75rem;
28
+
justify-content: space-between;
29
+
align-items: center;
30
+
color: var(--text-light);
33
+
.bsky-post-footer cite {
34
+
display: inline-flex;
35
+
align-items: center;
41
+
color: var(--text-light);
45
+
<div id="status-updates-container">
46
+
{% if response.records %}
47
+
{% for record in response.records | sort(attribute="value.createdAt") | reverse %}
48
+
{% set created_at = record.value.createdAt %}
49
+
{% set status_text = record.value.text %}
50
+
<div class="bsky-post" data-cid="{{ record.cid }}" data-created="{{ created_at }}">
51
+
<div class="bsky-post-content">{{ status_text }}</div>
52
+
<div class="bsky-post-footer">
55
+
src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg"
56
+
alt="Kieran's avatar"
59
+
<a href="https://bsky.app/@doing.dunkirk.sh" target="_blank" rel="noopener">@doing.dunkirk.sh</a>
61
+
<span class="bsky-post-time">
62
+
{{ record.value.createdAt | date(format="%b %d, %Y") }}
68
+
<div class="bsky-post">
69
+
<div class="bsky-post-content">No status updates found.</div>
75
+
document.addEventListener("DOMContentLoaded", () => {
76
+
const container = document.getElementById("status-updates-container");
77
+
const API_URL = "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update&limit=50";
78
+
const existingPosts = new Map();
80
+
// Collect existing posts by CID
81
+
document.querySelectorAll('.bsky-post[data-cid]').forEach(post => {
82
+
existingPosts.set(post.dataset.cid, {
84
+
created: new Date(post.dataset.created)
88
+
// Format time relative to now
89
+
function formatTimeAgo(date) {
90
+
const now = new Date();
91
+
const diffInMs = now - date;
92
+
const diffInMins = Math.floor(diffInMs / (1000 * 60));
93
+
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
95
+
if (diffInMins < 1) return "just now";
96
+
if (diffInMins < 60) return `${Math.round(diffInMins)}m`;
97
+
if (diffInHours < 24) return `${Math.round(diffInHours)}h`;
99
+
return new Intl.DateTimeFormat("en", {
105
+
// Update timestamps on existing posts
106
+
function updateTimestamps() {
107
+
existingPosts.forEach((post) => {
108
+
const timeElement = post.element.querySelector('.bsky-post-time');
110
+
timeElement.textContent = formatTimeAgo(post.created);
115
+
// Create a new post element
116
+
function createPostElement(record) {
117
+
const createdDate = new Date(record.value.createdAt);
118
+
const postElement = document.createElement('div');
119
+
postElement.className = 'bsky-post';
120
+
postElement.dataset.cid = record.cid;
121
+
postElement.dataset.created = record.value.createdAt;
123
+
postElement.innerHTML = `
124
+
<div class="bsky-post-content">${record.value.text}</div>
125
+
<div class="bsky-post-footer">
127
+
<img src="https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:3h24oe2owgmqpulq6dwwnsph/bafkreiaosnd5uyvwfii4ecb7zks67vwdiovnulsjnr6kb3azbfigjcaw5u@jpeg" alt="Kieran's avatar" class="avatar" />
128
+
<a href="https://bsky.app/@doing.dunkirk.sh" target="_blank" rel="noopener">@doing.dunkirk.sh</a>
130
+
<span class="bsky-post-time">${formatTimeAgo(createdDate)}</span>
134
+
return postElement;
137
+
// Fetch and update posts
138
+
function fetchAndUpdatePosts() {
140
+
.then(response => response.json())
142
+
if (!data.records || data.records.length === 0) {
143
+
if (existingPosts.size === 0) {
144
+
container.innerHTML = '<div class="bsky-post"><div class="bsky-post-content">No status updates found.</div></div>';
149
+
// Sort newest first
150
+
const sortedRecords = data.records.sort((a, b) => {
151
+
return new Date(b.value.createdAt) - new Date(a.value.createdAt);
154
+
// Track if we need to reorder
155
+
let needsReordering = false;
158
+
for (const record of sortedRecords) {
159
+
if (!existingPosts.has(record.cid)) {
160
+
const newPostElement = createPostElement(record);
161
+
// Always insert at the beginning for now (we'll reorder if needed)
162
+
container.insertBefore(newPostElement, container.firstChild);
163
+
existingPosts.set(record.cid, {
164
+
element: newPostElement,
165
+
created: new Date(record.value.createdAt)
167
+
needsReordering = true;
171
+
// If we added new posts, reorder everything
172
+
if (needsReordering) {
173
+
const sortedElements = [...existingPosts.entries()]
174
+
.sort((a, b) => b[1].created - a[1].created)
175
+
.map(entry => entry[1].element);
177
+
// Reattach in correct order
178
+
sortedElements.forEach(element => {
179
+
container.appendChild(element);
183
+
// Update all timestamps
184
+
updateTimestamps();
187
+
console.error("Error fetching status updates:", error);
192
+
fetchAndUpdatePosts();
194
+
// Update timestamps every minute
195
+
setInterval(updateTimestamps, 60000);
197
+
// Fetch new posts every 5 minutes
198
+
setInterval(fetchAndUpdatePosts, 300000);