slack status without the slack
status.zzstoatzz.io
hatk
statusphere
1// Beautiful timestamp formatting with hover tooltips
2// Provides minute-resolution display by default with full timestamp on hover
3
4const TimestampFormatter = {
5 // Format a timestamp with appropriate granularity
6 formatRelative(date, now = new Date()) {
7 const diffMs = now - date;
8 const diffSecs = Math.floor(diffMs / 1000);
9 const diffMins = Math.floor(diffMs / 60000);
10 const diffHours = Math.floor(diffMs / 3600000);
11 const diffDays = Math.floor(diffMs / 86400000);
12
13 // For very recent times, show "just now"
14 if (diffSecs < 30) {
15 return 'just now';
16 }
17
18 // Under 1 hour: show minutes
19 if (diffMins < 60) {
20 return `${diffMins}m ago`;
21 }
22
23 // Under 24 hours: show hours and minutes
24 if (diffHours < 24) {
25 const remainingMins = diffMins % 60;
26 if (remainingMins === 0) {
27 return `${diffHours}h ago`;
28 }
29 return `${diffHours}h ${remainingMins}m ago`;
30 }
31
32 // Under 7 days: show days and hours
33 if (diffDays < 7) {
34 const remainingHours = diffHours % 24;
35 if (remainingHours === 0) {
36 return `${diffDays}d ago`;
37 }
38 return `${diffDays}d ${remainingHours}h ago`;
39 }
40
41 // Over a week: show date with time
42 const timeStr = date.toLocaleTimeString('en-US', {
43 hour: 'numeric',
44 minute: '2-digit',
45 hour12: true
46 }).toLowerCase();
47
48 // If same year, don't show year
49 if (date.getFullYear() === now.getFullYear()) {
50 return date.toLocaleDateString('en-US', {
51 month: 'short',
52 day: 'numeric'
53 }) + ', ' + timeStr;
54 }
55
56 // Different year: show full date
57 return date.toLocaleDateString('en-US', {
58 month: 'short',
59 day: 'numeric',
60 year: 'numeric'
61 }) + ', ' + timeStr;
62 },
63
64 // Format future timestamps (for expiry times)
65 formatFuture(date, now = new Date()) {
66 const diffMs = date - now;
67 const diffSecs = Math.floor(diffMs / 1000);
68 const diffMins = Math.floor(diffMs / 60000);
69 const diffHours = Math.floor(diffMs / 3600000);
70 const diffDays = Math.floor(diffMs / 86400000);
71
72 if (diffSecs < 60) {
73 return 'expires soon';
74 }
75
76 if (diffMins < 60) {
77 return `expires in ${diffMins}m`;
78 }
79
80 if (diffHours < 24) {
81 const remainingMins = diffMins % 60;
82 if (remainingMins === 0) {
83 return `expires in ${diffHours}h`;
84 }
85 return `expires in ${diffHours}h ${remainingMins}m`;
86 }
87
88 if (diffDays < 7) {
89 const remainingHours = diffHours % 24;
90 if (remainingHours === 0) {
91 return `expires in ${diffDays}d`;
92 }
93 return `expires in ${diffDays}d ${remainingHours}h`;
94 }
95
96 // Over a week: show date
97 return 'expires ' + date.toLocaleDateString('en-US', {
98 month: 'short',
99 day: 'numeric',
100 hour: 'numeric',
101 minute: '2-digit',
102 hour12: true
103 }).toLowerCase();
104 },
105
106 // Format for history view (compact but informative)
107 formatCompact(date, now = new Date()) {
108 const diffMs = now - date;
109 const diffMins = Math.floor(diffMs / 60000);
110 const diffHours = Math.floor(diffMs / 3600000);
111 const diffDays = Math.floor(diffMs / 86400000);
112
113 // Today: show time only
114 if (date.toDateString() === now.toDateString()) {
115 return date.toLocaleTimeString('en-US', {
116 hour: 'numeric',
117 minute: '2-digit',
118 hour12: true
119 }).toLowerCase();
120 }
121
122 // Yesterday: show "yesterday" + time
123 const yesterday = new Date(now);
124 yesterday.setDate(yesterday.getDate() - 1);
125 if (date.toDateString() === yesterday.toDateString()) {
126 return 'yesterday, ' + date.toLocaleTimeString('en-US', {
127 hour: 'numeric',
128 minute: '2-digit',
129 hour12: true
130 }).toLowerCase();
131 }
132
133 // Within 7 days: show day of week + time
134 if (diffDays < 7) {
135 const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase();
136 const time = date.toLocaleTimeString('en-US', {
137 hour: 'numeric',
138 minute: '2-digit',
139 hour12: true
140 }).toLowerCase();
141 return `${dayName}, ${time}`;
142 }
143
144 // Same year: show month, day, time
145 if (date.getFullYear() === now.getFullYear()) {
146 return date.toLocaleDateString('en-US', {
147 month: 'short',
148 day: 'numeric',
149 hour: 'numeric',
150 minute: '2-digit',
151 hour12: true
152 }).toLowerCase();
153 }
154
155 // Different year: show full date
156 return date.toLocaleDateString('en-US', {
157 month: 'short',
158 day: 'numeric',
159 year: 'numeric',
160 hour: 'numeric',
161 minute: '2-digit',
162 hour12: true
163 }).toLowerCase();
164 },
165
166 // Get full timestamp for tooltip
167 getFullTimestamp(date) {
168 // Get day of week
169 const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });
170
171 // Get month and day
172 const monthDay = date.toLocaleDateString('en-US', {
173 month: 'long',
174 day: 'numeric',
175 year: 'numeric'
176 });
177
178 // Get time with seconds
179 const time = date.toLocaleTimeString('en-US', {
180 hour: 'numeric',
181 minute: '2-digit',
182 second: '2-digit',
183 hour12: true
184 });
185
186 // Get timezone
187 const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
188 const tzAbbr = date.toLocaleTimeString('en-US', {
189 timeZoneName: 'short'
190 }).split(' ').pop();
191
192 return `${dayName}, ${monthDay} at ${time} ${tzAbbr}`;
193 },
194
195 // Initialize all timestamps on the page
196 initialize() {
197 const updateTimestamps = () => {
198 const now = new Date();
199
200 document.querySelectorAll('.local-time').forEach(el => {
201 const timestamp = el.getAttribute('data-timestamp');
202 if (!timestamp) return;
203
204 const date = new Date(timestamp);
205 const format = el.getAttribute('data-format');
206 const prefix = el.getAttribute('data-prefix');
207
208 let text = '';
209
210 // Determine format type
211 if (prefix === 'expires' || prefix === 'clears') {
212 text = this.formatFuture(date, now);
213 } else if (format === 'compact' || format === 'short') {
214 text = this.formatCompact(date, now);
215 } else if (prefix === 'since') {
216 const relativeText = this.formatRelative(date, now);
217 text = `since ${relativeText}`.replace('since just now', 'just started');
218 } else {
219 text = this.formatRelative(date, now);
220 }
221
222 // Update text content
223 el.textContent = text;
224
225 // Add tooltip with full timestamp
226 const fullTimestamp = this.getFullTimestamp(date);
227 el.setAttribute('title', fullTimestamp);
228 el.style.cursor = 'help';
229 el.style.display = 'inline-block';
230 el.style.lineHeight = '1.2';
231 el.style.alignSelf = 'flex-start';
232 el.style.width = 'auto';
233 });
234 };
235
236 // Initial update
237 updateTimestamps();
238
239 // Update every 30 seconds for better granularity
240 setInterval(updateTimestamps, 30000);
241 }
242};
243
244// Auto-initialize when DOM is ready
245if (document.readyState === 'loading') {
246 document.addEventListener('DOMContentLoaded', () => TimestampFormatter.initialize());
247} else {
248 TimestampFormatter.initialize();
249}