a6b3f039d8
Pages: /, /simple, /decimal, /seth, /calendar, /astro, /convert, /timegov Features: Seth Calendar (10×36 + holidays), decimal time, moon phases, astronomy (sun/moon), bidirectional time converter, Seth date display, leap day split cell in calendar grid.
92 lines
2.6 KiB
JavaScript
92 lines
2.6 KiB
JavaScript
const zoneSelect = document.getElementById("zoneSelect");
|
|
const zoneLabel = document.getElementById("zoneLabel");
|
|
const offsetLabel = document.getElementById("offsetLabel");
|
|
const utcLabel = document.getElementById("utcLabel");
|
|
const dateLine = document.getElementById("dateLine");
|
|
const digital = document.getElementById("digital");
|
|
|
|
const zones = [
|
|
"America/New_York",
|
|
"America/Chicago",
|
|
"America/Denver",
|
|
"America/Los_Angeles",
|
|
"America/Anchorage",
|
|
"Pacific/Honolulu",
|
|
"UTC"
|
|
];
|
|
|
|
let serverOffsetMs = 0;
|
|
let selectedZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "America/New_York";
|
|
|
|
if (!zones.includes(selectedZone)) {
|
|
selectedZone = "America/New_York";
|
|
}
|
|
|
|
for (const zone of zones) {
|
|
const option = document.createElement("option");
|
|
option.value = zone;
|
|
option.textContent = zone;
|
|
if (zone === selectedZone) {
|
|
option.selected = true;
|
|
}
|
|
zoneSelect.appendChild(option);
|
|
}
|
|
|
|
zoneSelect.addEventListener("change", () => {
|
|
selectedZone = zoneSelect.value;
|
|
zoneLabel.textContent = selectedZone;
|
|
});
|
|
|
|
async function syncWithServer() {
|
|
const start = Date.now();
|
|
try {
|
|
const response = await fetch(`/api/time?ts=${start}`, { cache: "no-store" });
|
|
const end = Date.now();
|
|
const data = await response.json();
|
|
const rtt = end - start;
|
|
const estimatedServerAtEnd = data.epoch_ms + rtt / 2;
|
|
serverOffsetMs = estimatedServerAtEnd - end;
|
|
offsetLabel.textContent = `Synchronized (${serverOffsetMs >= 0 ? "+" : ""}${serverOffsetMs.toFixed(0)} ms, RTT ${rtt} ms)`;
|
|
} catch (_error) {
|
|
offsetLabel.textContent = "Sync failed";
|
|
}
|
|
}
|
|
|
|
function formatParts(date, zone) {
|
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: zone,
|
|
hour12: false,
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "2-digit",
|
|
weekday: "long",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
|
|
return formatter.formatToParts(date).reduce((accumulator, part) => {
|
|
if (part.type !== "literal") {
|
|
accumulator[part.type] = part.value;
|
|
}
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
|
|
function render() {
|
|
const now = new Date(Date.now() + serverOffsetMs);
|
|
const parts = formatParts(now, selectedZone);
|
|
const centiseconds = String(Math.floor(now.getMilliseconds() / 10)).padStart(2, "0");
|
|
|
|
dateLine.textContent = `${parts.weekday}, ${parts.month} ${parts.day}, ${parts.year}`;
|
|
digital.textContent = `${parts.hour}:${parts.minute}:${parts.second}.${centiseconds}`;
|
|
zoneLabel.textContent = selectedZone;
|
|
utcLabel.textContent = now.toISOString().replace("T", " ").replace("Z", " UTC");
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
syncWithServer();
|
|
setInterval(syncWithServer, 20000);
|
|
render();
|