JavaScript-optional public web frontend for Bluesky anartia.kelinci.net
sveltekit atcute bluesky typescript svelte
at trunk 224 lines 4.5 kB view raw
1// @ts-check 2 3let startOfYear = 0; 4let endOfYear = 0; 5 6const fmtTime = new Intl.DateTimeFormat('en-US', { 7 timeStyle: 'short', 8}); 9const fmtDateTime = new Intl.DateTimeFormat('en-US', { 10 dateStyle: 'long', 11 timeStyle: 'short', 12}); 13const fmtShortDateWithYear = new Intl.DateTimeFormat('en-US', { 14 dateStyle: 'medium', 15}); 16const fmtShortDate = new Intl.DateTimeFormat('en-US', { 17 month: 'short', 18 day: 'numeric', 19}); 20 21/** 22 * @param {string | number} date 23 * @returns {string} 24 */ 25const formatShortDate = (date) => { 26 const inst = new Date(date); 27 const time = inst.getTime(); 28 29 if (Number.isNaN(time)) { 30 return 'N/A'; 31 } 32 33 const now = Date.now(); 34 if (now > endOfYear) { 35 const date = new Date(now); 36 37 date.setMonth(0, 1); 38 date.setHours(0, 0, 0); 39 startOfYear = date.getTime(); 40 41 date.setFullYear(date.getFullYear() + 1, 0, 0); 42 date.setHours(23, 59, 59, 999); 43 endOfYear = date.getTime(); 44 } 45 46 if (time >= startOfYear && time <= endOfYear) { 47 return fmtShortDate.format(inst); 48 } 49 50 return fmtShortDateWithYear.format(inst); 51}; 52 53/** 54 * @param {string | number} date 55 * @returns {string} 56 */ 57const formatTime = (date) => { 58 const inst = new Date(date); 59 60 if (Number.isNaN(inst.getTime())) { 61 return 'N/A'; 62 } 63 64 return fmtTime.format(inst); 65}; 66 67/** 68 * @param {string | number} date 69 * @returns {string} 70 */ 71const formatLongDate = (date) => { 72 const inst = new Date(date); 73 74 if (Number.isNaN(inst.getTime())) { 75 return 'N/A'; 76 } 77 78 return fmtDateTime.format(inst); 79}; 80 81/** @type {Record<string, Intl.NumberFormat>} */ 82const relativeFormatters = {}; 83 84const SECOND = 1e3; 85const NOW = SECOND * 10; 86const MINUTE = SECOND * 60; 87const HOUR = MINUTE * 60; 88const DAY = HOUR * 24; 89const WEEK = DAY * 7; 90 91/** 92 * @param {string | number} date 93 * @param {number} now 94 * @returns {string} 95 */ 96const formatRelativeTime = (date, now) => { 97 const time = new Date(date).getTime(); 98 99 if (Number.isNaN(time)) { 100 return 'N/A'; 101 } 102 103 const delta = now - time; 104 105 if (delta < -NOW || delta > WEEK) { 106 if (now > endOfYear) { 107 const date = new Date(); 108 109 date.setMonth(0, 1); 110 date.setHours(0, 0, 0); 111 startOfYear = date.getTime(); 112 113 date.setFullYear(date.getFullYear() + 1, 0, 0); 114 date.setHours(23, 59, 59, 999); 115 endOfYear = date.getTime(); 116 } 117 118 // if it happened this year, don't show the year. 119 if (time >= startOfYear && time <= endOfYear) { 120 return fmtShortDate.format(time); 121 } 122 123 return fmtShortDateWithYear.format(time); 124 } 125 126 if (delta < NOW) { 127 return `now`; 128 } 129 130 { 131 /** @type {number} */ 132 let value; 133 /** @type {Intl.RelativeTimeFormatUnit} */ 134 let unit; 135 136 if (delta < MINUTE) { 137 value = Math.floor(delta / SECOND); 138 unit = 'second'; 139 } else if (delta < HOUR) { 140 value = Math.floor(delta / MINUTE); 141 unit = 'minute'; 142 } else if (delta < DAY) { 143 value = Math.floor(delta / HOUR); 144 unit = 'hour'; 145 } else { 146 // use rounding, this handles the following scenario: 147 // - 2024-02-13T09:00Z <- 2024-02-15T07:00Z = 2d 148 value = Math.round(delta / DAY); 149 unit = 'day'; 150 } 151 152 const formatter = (relativeFormatters[unit] ||= new Intl.NumberFormat('en-US', { 153 style: 'unit', 154 unit: unit, 155 unitDisplay: 'narrow', 156 })); 157 158 return formatter.format(Math.abs(value)); 159 } 160}; 161 162(() => { 163 /** @type {NodeListOf<HTMLTimeElement>} */ 164 const nodes = document.querySelectorAll('time[data-format="short-date"]'); 165 if (nodes.length === 0) { 166 return; 167 } 168 169 for (const node of nodes) { 170 const dt = node.dateTime; 171 172 node.textContent = formatShortDate(dt); 173 node.title = formatLongDate(dt); 174 } 175})(); 176 177(() => { 178 /** @type {NodeListOf<HTMLTimeElement>} */ 179 const nodes = document.querySelectorAll('time[data-format="long-date"]'); 180 if (nodes.length === 0) { 181 return; 182 } 183 184 for (const node of nodes) { 185 node.textContent = formatLongDate(node.dateTime); 186 } 187})(); 188 189(() => { 190 /** @type {NodeListOf<HTMLTimeElement>} */ 191 const nodes = document.querySelectorAll('time[data-format="time"]'); 192 if (nodes.length === 0) { 193 return; 194 } 195 196 for (const node of nodes) { 197 const dt = node.dateTime; 198 199 node.textContent = formatTime(dt); 200 node.title = formatLongDate(dt); 201 } 202})(); 203 204(() => { 205 /** @type {NodeListOf<HTMLTimeElement>} */ 206 const nodes = document.querySelectorAll('time[data-format="relative-time"]'); 207 if (nodes.length === 0) { 208 return; 209 } 210 211 const update = () => { 212 const now = Date.now(); 213 214 for (const node of nodes) { 215 const dt = node.dateTime; 216 217 node.textContent = formatRelativeTime(dt, now); 218 node.title = formatLongDate(dt); 219 } 220 }; 221 222 update(); 223 setInterval(update, 60_000); 224})();