Skip to content

Commit ffb211a

Browse files
authored
Add wdtime.js to format timestamps in the client-side browser (#33)
1 parent f880371 commit ffb211a

7 files changed

Lines changed: 159 additions & 3 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Package.resolved
3131

3232
.build/
3333

34+
# Swiftly
35+
.swift-version
36+
3437
# CocoaPods
3538
#
3639
# We recommend against adding the Pods directory to your .gitignore. However

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
### Added
10+
* Localization of all dates to the user's browser locale with JavaScript.
11+
812
## [0.1.3] - 2025-03-31
913
### Removed
1014
* Moved CI release action into a reusable workflow in another repository: https://github.com/BertanT/SwiftPM-Cross-Comp-CI.

Sources/HTML/WDHTML.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ struct WDHTML {
3535

3636
// Set up external sources
3737
.stylesheet("./styles/wdstyle.css"),
38+
.script(.src("./scripts/wdtime.js")),
3839

3940
.favicon("./images/wdfavicon.png"),
4041

Sources/HTML/WDPageListSubtext.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@ struct WDPageListSubtext: Component {
2424

2525
var body: Component {
2626
ComponentGroup {
27-
H6("Last updated: \(lastUpdate.formatted(date: .abbreviated, time: .complete))")
27+
H6 {
28+
Text("Last updated: \(lastUpdate.formatted(date: .abbreviated, time: .complete))")
29+
}
30+
.attribute(named: "data-timestamp", value: "\(Int(lastUpdate.timeIntervalSince1970))")
31+
.attribute(named: "data-format-type", value: "lastupdated")
32+
2833
H6("All logs are shown in \(TimeZone.current.abbreviation() ?? "the server's local time zone").")
34+
.class("tz-notice-nojs")
35+
36+
H6("All logs are shown in your browser's timezone.")
37+
.class("tz-notice-js")
2938
}
3039
.class("help-text")
3140
}

Sources/HTML/WDPageStatusList.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,17 @@ struct WDPageStatusList: Component {
5656
List {
5757
for outage in status.outages {
5858
if let end = outage.end {
59-
ListItem("\(outage.start.formatted()) - \(end.formatted())")
59+
ListItem {
60+
Text("\(outage.start.formatted()) - \(end.formatted())")
61+
}
62+
.attribute(named: "data-timestamp", value: "\(Int(outage.start.timeIntervalSince1970))")
63+
.attribute(named: "data-end-timestamp", value: "\(Int(end.timeIntervalSince1970))")
6064
} else {
61-
ListItem("Down since \(outage.start.formatted())")
65+
ListItem {
66+
Text("Down since \(outage.start.formatted())")
67+
}
68+
.attribute(named: "data-timestamp", value: "\(Int(outage.start.timeIntervalSince1970))")
69+
.attribute(named: "data-format-type", value: "ongoing")
6270
}
6371
}
6472
}

Sources/Resources/DefaultHTMLAssets/wdstyle.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ html, body {
298298
opacity: 100%;
299299
}
300300

301+
.tz-notice-js {
302+
display: none;
303+
}
304+
301305
.help-text {
302306
color: var(--secondary-text-color);
303307
font-size: 0.8rem;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
wdtime.js
3+
4+
Copyright (C) 2025 Bedir Ekim
5+
6+
This file is part of WatchDuck.
7+
8+
WatchDuck is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
9+
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10+
11+
WatchDuck is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
12+
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
13+
14+
You should have received a copy of the GNU Affero General Public License along with WatchDuck.
15+
If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
document.addEventListener('DOMContentLoaded', function() {
19+
formatAllTimestamps();
20+
});
21+
22+
/**
23+
* Format all timestamp elements on the page.
24+
*/
25+
function formatAllTimestamps() {
26+
document.querySelectorAll('[data-timestamp]').forEach(formatTimestampElement);
27+
28+
// Make sure the time zone notice is correct for users who don't have JS enabled
29+
document.querySelectorAll('.tz-notice-nojs').forEach(e => e.style.display = 'none');
30+
document.querySelectorAll('.tz-notice-js').forEach(e => e.style.display = 'block');
31+
}
32+
33+
function formatTimestampElement(element) {
34+
const timestamp = parseInt(element.getAttribute('data-timestamp'), 10) * 1000;
35+
const date = new Date(timestamp);
36+
37+
const formatType = element.getAttribute('data-format-type') || 'datetime';
38+
const isRange = element.hasAttribute('data-end-timestamp');
39+
40+
if (isRange) {
41+
const endTimestamp = parseInt(element.getAttribute('data-end-timestamp'), 10) * 1000;
42+
const endDate = new Date(endTimestamp);
43+
element.textContent = formatDateRange(date, endDate);
44+
} else if (formatType === 'datetime') {
45+
element.textContent = formatDateTime(date, true);
46+
} else if (formatType === 'lastupdated') {
47+
element.textContent = 'Last updated: ' + formatDateTime(date, true);
48+
} else if (formatType === 'ongoing') {
49+
element.textContent = 'Down since ' + formatDateTime(date);
50+
}
51+
}
52+
53+
/**
54+
* Format a date in a user-friendly format.
55+
* @param {Date} date - Date to format.
56+
* @param {Boolean} includeTimezone - Whether to include timezone information.
57+
* @param {Boolean} isOutageTime - Whether this is for an outage timestamp.
58+
* @returns {string} Formatted date string.
59+
*/
60+
function formatDateTime(date, includeTimezone = false, isOutageTime = false) {
61+
if (isOutageTime) {
62+
const dateStr = date.toLocaleDateString();
63+
const timeStr = date.toLocaleTimeString(undefined, {
64+
hour: '2-digit',
65+
minute: '2-digit'
66+
});
67+
return `${dateStr}, ${timeStr}`;
68+
}
69+
70+
const options = {
71+
year: 'numeric',
72+
month: 'short',
73+
day: 'numeric',
74+
hour: '2-digit',
75+
minute: '2-digit',
76+
second: '2-digit'
77+
};
78+
79+
const formattedDateTime = date.toLocaleString(undefined, options);
80+
81+
if (includeTimezone) {
82+
const tzString = getTimezoneString(date);
83+
return `${formattedDateTime} ${tzString}`;
84+
}
85+
86+
return formattedDateTime;
87+
}
88+
89+
/**
90+
* Get the timezone string (like GMT+2) for a date.
91+
* @param {Date} date - The date to get timezone for.
92+
* @returns {string} Timezone string.
93+
*/
94+
function getTimezoneString(date) {
95+
const timezoneOffset = date.getTimezoneOffset();
96+
const offsetHours = Math.abs(Math.floor(timezoneOffset / 60));
97+
const offsetMinutes = Math.abs(timezoneOffset % 60);
98+
const offsetSign = timezoneOffset <= 0 ? '+' : '-';
99+
100+
if (offsetMinutes === 0) {
101+
return `GMT${offsetSign}${offsetHours}`;
102+
} else {
103+
return `GMT${offsetSign}${offsetHours}:${offsetMinutes.toString().padStart(2, '0')}`;
104+
}
105+
}
106+
107+
/**
108+
* Format a date range.
109+
* @param {Date} startDate - Start date.
110+
* @param {Date} endDate - End date.
111+
* @returns {string} Formatted date range.
112+
*/
113+
function formatDateRange(startDate, endDate) {
114+
if (startDate.getFullYear() === endDate.getFullYear() &&
115+
startDate.getMonth() === endDate.getMonth() &&
116+
startDate.getDate() === endDate.getDate() &&
117+
startDate.getHours() === endDate.getHours() &&
118+
startDate.getMinutes() === endDate.getMinutes()) {
119+
120+
const formattedStart = formatDateTime(startDate, false, true);
121+
return `${formattedStart} - Resolved under a minute :)`;
122+
}
123+
124+
const formattedStart = formatDateTime(startDate, false, true);
125+
const formattedEnd = formatDateTime(endDate, false, true);
126+
return `${formattedStart} - ${formattedEnd}`;
127+
}

0 commit comments

Comments
 (0)