Initial commit — Seth Calendar & Decimal Time clock site
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.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user