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>