diff --git a/app/javascript/helpers/date_helpers.js b/app/javascript/helpers/date_helpers.js index b8d9855f47..923a7b6ca2 100644 --- a/app/javascript/helpers/date_helpers.js +++ b/app/javascript/helpers/date_helpers.js @@ -1,3 +1,5 @@ +import { getMetaContent } from "helpers/meta_helpers" + export function differenceInDays(fromDate, toDate) { return Math.round(Math.abs((beginningOfDay(toDate) - beginningOfDay(fromDate)) / (1000 * 60 * 60 * 24))) } @@ -7,9 +9,44 @@ export function signedDifferenceInDays(fromDate, toDate) { } export function beginningOfDay(date) { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const { year, month, day } = datePartsInTimezone(date) + return new Date(Date.UTC(year, month - 1, day)) } export function secondsToDate(seconds) { return new Date(seconds * 1000) } + +// Snap a timestamp to midnight using the timezone the server rendered with (the +// `timezone` meta tag), so client day boundaries match the server's instead of +// following the browser's resolved timezone, which can differ in a PWA. +function datePartsInTimezone(date) { + return dateFormatter().formatToParts(date).reduce((parts, { type, value }) => { + if (type !== "literal") parts[type] = parseInt(value, 10) + return parts + }, {}) +} + +let dateFormatterCache +let dateFormatterTimezone + +function dateFormatter() { + const timezone = getMetaContent("timezone") + + if (!dateFormatterCache || dateFormatterTimezone !== timezone) { + dateFormatterTimezone = timezone + dateFormatterCache = buildDateFormatter(timezone) + } + + return dateFormatterCache +} + +function buildDateFormatter(timezone) { + const options = { year: "numeric", month: "2-digit", day: "2-digit" } + + try { + return new Intl.DateTimeFormat("en-US", { ...options, timeZone: timezone }) + } catch { + return new Intl.DateTimeFormat("en-US", options) + } +} diff --git a/app/javascript/helpers/meta_helpers.js b/app/javascript/helpers/meta_helpers.js new file mode 100644 index 0000000000..a53f7312a9 --- /dev/null +++ b/app/javascript/helpers/meta_helpers.js @@ -0,0 +1,3 @@ +export function getMetaContent(name) { + return document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") +} diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb index 9ef153df2b..bddd34b4a4 100644 --- a/app/views/layouts/shared/_head.html.erb +++ b/app/views/layouts/shared/_head.html.erb @@ -11,6 +11,7 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= tag.meta name: "current-user-id", content: Current.user.id if Current.user %> + <%= tag.meta name: "timezone", content: Time.zone.name %> <%= tag.meta name: "vapid-public-key", content: Rails.configuration.x.vapid.public_key %> <% turbo_refreshes_with method: :morph, scroll: :preserve %>