tangled
alpha
login
or
join now
dunkirk.sh
/
zera
5
fork
atom
the home site for me: also iteration 3 or 4 of my site
5
fork
atom
overview
issues
pulls
pipelines
feat: fancy relative time
dunkirk.sh
2 months ago
2b7a1b73
c3e56e41
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+132
-71
4 changed files
expand all
collapse all
unified
split
sass
css
mods.css
static
js
relative-time.js
templates
header.html
shortcodes
is.md
+2
sass/css/mods.css
···
345
345
text-decoration: none;
346
346
}
347
347
348
348
+
349
349
+
348
350
.bubble > span {
349
351
display: flex;
350
352
flex-wrap: wrap;
+127
-62
static/js/relative-time.js
···
1
1
-
const rtf = new Intl.RelativeTimeFormat(navigator.language, {
2
2
-
numeric: "auto",
3
3
-
style: "long"
4
4
-
});
1
1
+
class RelativeTimeElement extends HTMLElement {
2
2
+
static get observedAttributes() {
3
3
+
return ['datetime', 'threshold', 'prefix', 'format'];
4
4
+
}
5
5
6
6
-
function formatRelativeTime(date) {
7
7
-
const now = new Date();
8
8
-
const diffInMs = now - date;
9
9
-
const diffInMins = Math.floor(diffInMs / (1000 * 60));
10
10
-
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
11
11
-
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
6
6
+
connectedCallback() {
7
7
+
this.update();
8
8
+
}
9
9
+
10
10
+
disconnectedCallback() {
11
11
+
this.stopTimer();
12
12
+
}
13
13
+
14
14
+
attributeChangedCallback() {
15
15
+
this.update();
16
16
+
}
17
17
+
18
18
+
scheduleUpdate(ms) {
19
19
+
this.stopTimer();
20
20
+
this.timer = setTimeout(() => this.update(), ms);
21
21
+
}
22
22
+
23
23
+
stopTimer() {
24
24
+
if (this.timer) {
25
25
+
clearTimeout(this.timer);
26
26
+
this.timer = null;
27
27
+
}
28
28
+
}
29
29
+
30
30
+
get datetime() {
31
31
+
return this.getAttribute('datetime') || '';
32
32
+
}
33
33
+
34
34
+
get threshold() {
35
35
+
return this.getAttribute('threshold') || 'P30D';
36
36
+
}
37
37
+
38
38
+
get prefix() {
39
39
+
return this.getAttribute('prefix') || 'on';
40
40
+
}
41
41
+
42
42
+
get format() {
43
43
+
return this.getAttribute('format') || 'relative';
44
44
+
}
12
45
13
13
-
if (diffInMins < 1) {
14
14
-
return rtf.format(0, "minute");
15
15
-
} else if (diffInMins < 5) {
16
16
-
return rtf.format(-1, "minute");
17
17
-
} else if (diffInMins < 60) {
18
18
-
return rtf.format(-diffInMins, "minute");
19
19
-
} else if (diffInHours < 3) {
20
20
-
return rtf.format(-diffInHours, "hour");
21
21
-
} else if (diffInHours < 24 && now.getDate() === date.getDate()) {
22
22
-
const hour = date.getHours();
23
23
-
if (hour < 12) return "this morning";
24
24
-
if (hour < 17) return "this afternoon";
25
25
-
return "this evening";
26
26
-
} else if (diffInDays < 2) {
27
27
-
return rtf.format(-1, "day");
28
28
-
} else if (diffInDays < 7) {
29
29
-
return rtf.format(-diffInDays, "day");
30
30
-
} else {
31
31
-
const dateFormatter = new Intl.DateTimeFormat(navigator.language, {
32
32
-
month: 'short',
33
33
-
day: 'numeric'
34
34
-
});
35
35
-
return `on ${dateFormatter.format(date)}`;
46
46
+
parseThreshold(iso) {
47
47
+
const match = iso.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
48
48
+
if (!match) return 30 * 24 * 60 * 60 * 1000;
49
49
+
const days = parseInt(match[1] || 0, 10);
50
50
+
const hours = parseInt(match[2] || 0, 10);
51
51
+
const minutes = parseInt(match[3] || 0, 10);
52
52
+
const seconds = parseInt(match[4] || 0, 10);
53
53
+
return ((days * 24 + hours) * 60 + minutes) * 60 * 1000 + seconds * 1000;
36
54
}
37
37
-
}
38
55
39
39
-
function updateTimeElements() {
40
40
-
document.querySelectorAll("time[datetime]").forEach(el => {
41
41
-
const datetime = el.getAttribute("datetime");
56
56
+
update() {
57
57
+
const datetime = this.datetime;
42
58
if (!datetime) return;
43
59
44
60
const date = new Date(datetime);
45
61
if (isNaN(date.getTime())) return;
46
62
47
47
-
const maxAge = el.dataset.maxAge;
48
48
-
if (maxAge) {
49
49
-
const diffInMs = Date.now() - date.getTime();
50
50
-
const maxAgeMs = parseInt(maxAge, 10) * 1000;
51
51
-
if (diffInMs > maxAgeMs) {
52
52
-
el.style.display = "none";
53
53
-
return;
54
54
-
}
63
63
+
const now = Date.now();
64
64
+
const diff = now - date.getTime();
65
65
+
const absDiff = Math.abs(diff);
66
66
+
const thresholdMs = this.parseThreshold(this.threshold);
67
67
+
68
68
+
if (this.format === 'datetime' || absDiff > thresholdMs) {
69
69
+
this.textContent = this.formatDatetime(date);
70
70
+
this.scheduleUpdate(3600000);
71
71
+
} else {
72
72
+
this.textContent = this.formatRelative(diff);
73
73
+
this.scheduleUpdate(this.getNextUpdateDelay(absDiff));
55
74
}
75
75
+
}
56
76
57
57
-
el.textContent = formatRelativeTime(date);
58
58
-
el.style.display = "";
59
59
-
});
60
60
-
}
77
77
+
getNextUpdateDelay(absDiff) {
78
78
+
const seconds = Math.floor(absDiff / 1000);
79
79
+
const minutes = Math.floor(seconds / 60);
80
80
+
const hours = Math.floor(minutes / 60);
81
81
+
const days = Math.floor(hours / 24);
61
82
62
62
-
const observer = new MutationObserver((mutations) => {
63
63
-
for (const mutation of mutations) {
64
64
-
if (mutation.type === "attributes" && mutation.attributeName === "datetime") {
65
65
-
updateTimeElements();
66
66
-
return;
83
83
+
if (seconds < 60) {
84
84
+
return 1000;
85
85
+
} else if (minutes < 60) {
86
86
+
return 60000;
87
87
+
} else if (hours < 24) {
88
88
+
return 60000 * 5;
89
89
+
} else if (days < 7) {
90
90
+
return 3600000;
91
91
+
} else {
92
92
+
return 3600000 * 6;
67
93
}
68
94
}
69
69
-
});
70
70
-
observer.observe(document.documentElement, {
71
71
-
subtree: true,
72
72
-
attributes: true,
73
73
-
attributeFilter: ["datetime"]
74
74
-
});
75
95
76
76
-
document.addEventListener("DOMContentLoaded", updateTimeElements);
77
77
-
setInterval(updateTimeElements, 60000);
96
96
+
formatRelative(diff) {
97
97
+
const rtf = new Intl.RelativeTimeFormat(navigator.language, {
98
98
+
numeric: 'auto',
99
99
+
style: 'long'
100
100
+
});
101
101
+
102
102
+
const absDiff = Math.abs(diff);
103
103
+
const sign = diff > 0 ? -1 : 1;
104
104
+
const seconds = Math.floor(absDiff / 1000);
105
105
+
const minutes = Math.floor(seconds / 60);
106
106
+
const hours = Math.floor(minutes / 60);
107
107
+
const days = Math.floor(hours / 24);
108
108
+
const months = Math.floor(days / 30);
109
109
+
const years = Math.floor(days / 365);
110
110
+
111
111
+
if (seconds < 60) {
112
112
+
return rtf.format(sign * seconds, 'second');
113
113
+
} else if (minutes < 60) {
114
114
+
return rtf.format(sign * minutes, 'minute');
115
115
+
} else if (hours < 24) {
116
116
+
return rtf.format(sign * hours, 'hour');
117
117
+
} else if (days < 30) {
118
118
+
return rtf.format(sign * days, 'day');
119
119
+
} else if (months < 12) {
120
120
+
return rtf.format(sign * months, 'month');
121
121
+
} else {
122
122
+
return rtf.format(sign * years, 'year');
123
123
+
}
124
124
+
}
125
125
+
126
126
+
formatDatetime(date) {
127
127
+
const now = new Date();
128
128
+
const sameYear = date.getFullYear() === now.getFullYear();
129
129
+
130
130
+
const options = {
131
131
+
month: 'short',
132
132
+
day: 'numeric',
133
133
+
...(sameYear ? {} : { year: 'numeric' })
134
134
+
};
135
135
+
136
136
+
const prefix = this.prefix;
137
137
+
const formatted = new Intl.DateTimeFormat(navigator.language, options).format(date);
138
138
+
return prefix ? `${prefix} ${formatted}` : formatted;
139
139
+
}
140
140
+
}
141
141
+
142
142
+
customElements.define('relative-time', RelativeTimeElement);
+2
-2
templates/header.html
···
40
40
nowPlayingTimeout = setTimeout(fetchNowPlaying, 60000);
41
41
return;
42
42
}
43
43
-
el.innerHTML = `🎵 <a href="${item.originUrl || '#'}" target="_blank" rel="noopener"><span class="track-name">${item.trackName}</span></a> - <span class="artist-name">${item.artists?.[0]?.artistName || 'Unknown'}</span>`;
44
44
-
const timeUntilExpiry = expiry - now + 1000;
43
43
+
el.innerHTML = `🎵 <a href="${item.originUrl || '#'}" target="_blank" rel="noopener"><span class="track-name">${item.trackName}</span></a> - <span class="artist-name">${item.artists?.[0]?.artistName || 'Unknown'}</span> <relative-time datetime="${item.playedTime}" threshold="P1D"></relative-time>`;
44
44
+
const timeUntilExpiry = expiry - now + 5000;
45
45
nowPlayingTimeout = setTimeout(fetchNowPlaying, timeUntilExpiry);
46
46
})
47
47
.catch(() => {
+1
-7
templates/shortcodes/is.md
···
1
1
<div class="bubble" style="visibility: hidden; opacity: 0;">
2
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-wrap"><span class="time-dash"> - </span><time id="time-ago" datetime="" data-max-age="43200"></time></span></span>
2
2
+
<span><span id="status-wrap"><a href="https://bsky.app/@doing.dunkirk.sh" id="verb-link">Kieran is</a> <i id="status-text"></i></span><span id="time-ago-wrap"><span class="time-dash"> - </span><relative-time id="time-ago" datetime="" threshold="P30D"></relative-time></span></span>
3
3
</div>
4
4
5
5
<script>
···
31
31
document.getElementById("status-text").textContent = latestStatus;
32
32
const timeEl = document.getElementById("time-ago");
33
33
timeEl.setAttribute("datetime", createdAt);
34
34
-
timeEl.textContent = new Intl.DateTimeFormat(navigator.language, {
35
35
-
month: 'short',
36
36
-
day: 'numeric',
37
37
-
hour: 'numeric',
38
38
-
minute: 'numeric'
39
39
-
}).format(createdDate);
40
34
const verbLink = document.getElementById("verb-link");
41
35
if (diffInMins > 30) {
42
36
verbLink.textContent = "Kieran was";