the home site for me: also iteration 3 or 4 of my site

feat: add time elements

dunkirk.sh d37d46f8 7ceabef6

verified
+94 -61
+77
static/js/relative-time.js
··· 1 + const rtf = new Intl.RelativeTimeFormat(navigator.language, { 2 + numeric: "auto", 3 + style: "long" 4 + }); 5 + 6 + function formatRelativeTime(date) { 7 + const now = new Date(); 8 + const diffInMs = now - date; 9 + const diffInMins = Math.floor(diffInMs / (1000 * 60)); 10 + const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); 11 + const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); 12 + 13 + if (diffInMins < 1) { 14 + return rtf.format(0, "minute"); 15 + } else if (diffInMins < 5) { 16 + return rtf.format(-1, "minute"); 17 + } else if (diffInMins < 60) { 18 + return rtf.format(-diffInMins, "minute"); 19 + } else if (diffInHours < 3) { 20 + return rtf.format(-diffInHours, "hour"); 21 + } else if (diffInHours < 24 && now.getDate() === date.getDate()) { 22 + const hour = date.getHours(); 23 + if (hour < 12) return "this morning"; 24 + if (hour < 17) return "this afternoon"; 25 + return "this evening"; 26 + } else if (diffInDays < 2) { 27 + return rtf.format(-1, "day"); 28 + } else if (diffInDays < 7) { 29 + return rtf.format(-diffInDays, "day"); 30 + } else { 31 + const dateFormatter = new Intl.DateTimeFormat(navigator.language, { 32 + month: 'short', 33 + day: 'numeric' 34 + }); 35 + return `on ${dateFormatter.format(date)}`; 36 + } 37 + } 38 + 39 + function updateTimeElements() { 40 + document.querySelectorAll("time[datetime]").forEach(el => { 41 + const datetime = el.getAttribute("datetime"); 42 + if (!datetime) return; 43 + 44 + const date = new Date(datetime); 45 + if (isNaN(date.getTime())) return; 46 + 47 + const maxAge = el.dataset.maxAge; 48 + if (maxAge) { 49 + const diffInMs = Date.now() - date.getTime(); 50 + const maxAgeMs = parseInt(maxAge, 10) * 1000; 51 + if (diffInMs > maxAgeMs) { 52 + el.style.display = "none"; 53 + return; 54 + } 55 + } 56 + 57 + el.textContent = formatRelativeTime(date); 58 + el.style.display = ""; 59 + }); 60 + } 61 + 62 + const observer = new MutationObserver((mutations) => { 63 + for (const mutation of mutations) { 64 + if (mutation.type === "attributes" && mutation.attributeName === "datetime") { 65 + updateTimeElements(); 66 + return; 67 + } 68 + } 69 + }); 70 + observer.observe(document.documentElement, { 71 + subtree: true, 72 + attributes: true, 73 + attributeFilter: ["datetime"] 74 + }); 75 + 76 + document.addEventListener("DOMContentLoaded", updateTimeElements); 77 + setInterval(updateTimeElements, 60000);
+4
templates/head.html
··· 84 84 base64=true) %} 85 85 <script src="{{ get_url(path='lightbox.js?' ~ lightboxJsHash, trailing_slash=false) | safe }}" defer></script> 86 86 87 + {% set relativeTimeJsHash = get_hash(path="js/relative-time.js", sha_type=256, 88 + base64=true) %} 89 + <script src="{{ get_url(path='js/relative-time.js?' ~ relativeTimeJsHash, trailing_slash=false) | safe }}" defer></script> 90 + 87 91 <script type="speculationrules"> 88 92 { 89 93 "prerender": [
+13 -61
templates/shortcodes/is.md
··· 1 1 <div class="bubble" style="visibility: hidden; opacity: 0;"> 2 - <span><a href="https://bsky.app/@doing.dunkirk.sh" id="verb-link">Kieran is</a> <i id="status-text"></i> - <span id="time-ago"></span></span> 2 + <span><a href="https://bsky.app/@doing.dunkirk.sh" id="verb-link">Kieran is</a> <i id="status-text"></i> - <time id="time-ago" datetime="" data-max-age="43200"></time></span> 3 3 </div> 4 4 5 5 <script> 6 6 document.addEventListener("DOMContentLoaded", () => { 7 - // Initialize RelativeTimeFormat with user's locale 8 - const rtf = new Intl.RelativeTimeFormat(navigator.language, { 9 - numeric: "auto", 10 - style: "long" 11 - }); 12 - 13 7 fetch( 14 8 "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=dunkirk.sh&collection=a.status.update", 15 9 ) ··· 21 15 }) 22 16 .then((statusData) => { 23 17 if (statusData.records && statusData.records.length > 0) { 24 - // Calculate time difference 25 18 if (statusData.records[0].value.createdAt) { 26 - const createdDate = new Date(statusData.records[0].value.createdAt); 19 + const createdAt = statusData.records[0].value.createdAt; 20 + const createdDate = new Date(createdAt); 27 21 const now = new Date(); 28 22 const diffInMs = now - createdDate; 29 23 const diffInMins = Math.floor(diffInMs / (1000 * 60)); 30 24 const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); 31 - const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); 32 - 33 - // Ignore if older than 12 hours 34 25 if (diffInHours > 12) { 35 26 return; 36 27 } 37 - 38 28 const latestStatus = `"${statusData.records[0].value.text}"`; 39 29 document.getElementById("status-text").textContent = latestStatus; 40 - 41 - // Format time contextually using Intl.RelativeTimeFormat 42 - let timeAgoText; 43 - const createdHour = createdDate.getHours(); 44 - const isToday = diffInHours < 24 && now.getDate() === createdDate.getDate(); 45 - const isYesterday = diffInHours >= 24 && diffInHours < 48 && 46 - (now.getDate() - createdDate.getDate() === 1 || 47 - (now.getDate() === 1 && new Date(now.getFullYear(), now.getMonth(), 0).getDate() === createdDate.getDate())); 48 - 49 - if (diffInMins < 1) { 50 - timeAgoText = rtf.format(0, "minute"); // "now" in the user's language 51 - } else if (diffInMins < 5) { 52 - timeAgoText = rtf.format(-1, "minute"); // "1 minute ago" in the user's language 53 - } else if (diffInMins < 60) { 54 - timeAgoText = rtf.format(-diffInMins, "minute"); 55 - } else if (diffInHours < 3) { 56 - timeAgoText = rtf.format(-diffInHours, "hour"); 57 - } else if (isToday) { 58 - // Time of day context, but still localized 59 - if (createdHour < 12) { 60 - timeAgoText = "this morning"; 61 - } else if (createdHour < 17) { 62 - timeAgoText = "this afternoon"; 63 - } else { 64 - timeAgoText = "this evening"; 65 - } 66 - } else if (isYesterday) { 67 - timeAgoText = rtf.format(-1, "day"); // "yesterday" in the user's language 68 - } else if (diffInDays < 7) { 69 - timeAgoText = rtf.format(-diffInDays, "day"); 70 - } else { 71 - // For older posts, use a date formatter 72 - const dateFormatter = new Intl.DateTimeFormat(navigator.language, { 73 - month: 'short', 74 - day: 'numeric' 75 - }); 76 - timeAgoText = `on ${dateFormatter.format(createdDate)}`; 77 - } 78 - 79 - document.getElementById("time-ago").textContent = timeAgoText; 80 - 81 - // Change "is" to "was" based on recency 30 + const timeEl = document.getElementById("time-ago"); 31 + timeEl.setAttribute("datetime", createdAt); 32 + timeEl.textContent = new Intl.DateTimeFormat(navigator.language, { 33 + month: 'short', 34 + day: 'numeric', 35 + hour: 'numeric', 36 + minute: 'numeric' 37 + }).format(createdDate); 82 38 const verbLink = document.getElementById("verb-link"); 83 39 if (diffInMins > 30) { 84 40 verbLink.textContent = "Kieran was"; 85 41 } 86 - 87 - // Show and animate the bubble since we have a valid status 88 42 const bubble = document.querySelector(".bubble"); 89 43 bubble.style.visibility = "visible"; 90 44 bubble.classList.add("animate-in"); 91 - 92 - // For reduced motion preferences, ensure the bubble is always visible 93 45 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 94 - bubble.style.transform = "none"; // Ensure no transform is applied 95 - bubble.style.opacity = "1"; // Ensure content is visible 46 + bubble.style.transform = "none"; 47 + bubble.style.opacity = "1"; 96 48 } 97 49 } 98 50 }