diff --git a/components/EventTime/patterns/EventTimeLong.js b/components/EventTime/patterns/EventTimeLong.js index a97a494d..64563887 100644 --- a/components/EventTime/patterns/EventTimeLong.js +++ b/components/EventTime/patterns/EventTimeLong.js @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; -import { useDateString, useTimeString, useTimeZone } from "@/lib/utils"; +import { useTimeString, useTimeZone } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; export default function EventTimeLong({ startDate, @@ -9,9 +10,9 @@ export default function EventTimeLong({ timezone, isSameDay, }) { - const localizedStartDate = useDateString(startDate); + const localizedStartDate = makeDateString(startDate); const localizedStartTime = useTimeString(startTime); - const localizedEndDate = useDateString(endDate); + const localizedEndDate = makeDateString(endDate); const localizedEndTime = useTimeString(endTime); const localizedTimezone = useTimeZone(timezone); diff --git a/components/EventTime/patterns/EventTimeShort/index.js b/components/EventTime/patterns/EventTimeShort/index.js index bd9db94d..aff7f824 100644 --- a/components/EventTime/patterns/EventTimeShort/index.js +++ b/components/EventTime/patterns/EventTimeShort/index.js @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; -import { makeDateObject, useGlobalData } from "@/lib/utils"; +import { useGlobalData } from "@/lib/utils"; +import { makeDateObject } from "@/helpers/dates"; import * as Styled from "./styles"; import { fallbackLng } from "@/lib/i18n/settings"; @@ -15,10 +16,10 @@ function renderShortDate({ month, day, year }) { export default function EventTimeShort({ startDate, endDate, isSameDay }) { const localeInfo = useGlobalData("localeInfo"); - const lang = localeInfo?.language || fallbackLng; - const endDateObject = makeDateObject(endDate, lang, true); + const locale = localeInfo?.language || fallbackLng; + const endDateObject = makeDateObject(endDate, { locale, isShort: true }); const startDateObject = !isSameDay - ? makeDateObject(startDate, lang, true) + ? makeDateObject(startDate, { locale, isShort: true }) : null; return ( diff --git a/components/content-blocks/Callout/CalloutEntry/index.js b/components/content-blocks/Callout/CalloutEntry/index.js index a97ead26..1ca0e0bc 100644 --- a/components/content-blocks/Callout/CalloutEntry/index.js +++ b/components/content-blocks/Callout/CalloutEntry/index.js @@ -2,26 +2,27 @@ import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { makeDateString, getSiteString, makeReleaseFeature } from "@/lib/utils"; +import { getSiteString, makeReleaseFeature } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; import * as Styled from "./styles"; import { Image } from "@rubin-epo/epo-react-lib"; import { useRelease } from "@/lib/api/noirlabReleases"; import { fallbackLng } from "@/lib/i18n/settings"; -const getDateString = (newsDate, eventStart, eventEnd, lang) => { +const getDateString = (newsDate, eventStart, eventEnd, locale) => { if (newsDate) { - return makeDateString(newsDate, lang); + return makeDateString(newsDate, { locale }); } if (eventStart && eventEnd) { - return `${makeDateString(eventStart, lang)} - ${makeDateString( + return `${makeDateString(eventStart, { locale })} - ${makeDateString( eventEnd, - lang + { locale } )}`; } if (eventEnd) { - return makeDateString(eventEnd, lang); + return makeDateString(eventEnd, { locale }); } }; diff --git a/components/content-blocks/GridBlock/NewsGrid.js b/components/content-blocks/GridBlock/NewsGrid.js index eef8a467..8b0c091d 100644 --- a/components/content-blocks/GridBlock/NewsGrid.js +++ b/components/content-blocks/GridBlock/NewsGrid.js @@ -1,13 +1,14 @@ import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { - makeDateString, makeTruncatedString, makeReleaseFeature, normalizeItemData, useGlobalData, useList, } from "@/lib/utils"; + +import { makeDateString } from "@/helpers/dates"; import { Grid } from "@rubin-epo/epo-react-lib"; import Tile from "@/atomic/Tile"; import Loader from "@/atomic/Loader"; @@ -16,7 +17,7 @@ import { fallbackLng } from "@/lib/i18n/settings"; const NewsGrid = ({ items = [], limit, listTypeId, sectionHandle, pageId }) => { const { t } = useTranslation(); const localeInfo = useGlobalData("localeInfo"); - const lang = localeInfo?.language || fallbackLng; + const locale = localeInfo?.language || fallbackLng; // get manually-curated data first let allItems = normalizeItemData(items); @@ -70,7 +71,7 @@ const NewsGrid = ({ items = [], limit, listTypeId, sectionHandle, pageId }) => { isFeature={i === 0} link={uri} pretitle={postType?.[0]?.title ? postType[0].title : " "} - subtitle={makeDateString(date || releaseDate, lang)} + subtitle={makeDateString(date || releaseDate, { locale })} text={makeTruncatedString(subtitle || description, 30)} title={title} type="news" diff --git a/components/content-blocks/PublicationsList/index.js b/components/content-blocks/PublicationsList/index.js index 08f10759..c13877e2 100644 --- a/components/content-blocks/PublicationsList/index.js +++ b/components/content-blocks/PublicationsList/index.js @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { Container } from "@rubin-epo/epo-react-lib"; import { mixedLinkShape } from "@/shapes/link"; -import { makeDateString } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; import * as Styled from "./styles"; export default function PublicationsListBlock({ diff --git a/components/content-blocks/Schedule/index.js b/components/content-blocks/Schedule/index.js index cfdaac33..4fb2c706 100644 --- a/components/content-blocks/Schedule/index.js +++ b/components/content-blocks/Schedule/index.js @@ -1,10 +1,10 @@ import PropTypes from "prop-types"; import styled from "styled-components"; import { Container } from "@rubin-epo/epo-react-lib"; -import { useDateString } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; export default function ScheduleBlock({ date, description, scheduleRows }) { - const localizedDate = useDateString(date); + const localizedDate = makeDateString(date); return ( {localizedDate} diff --git a/components/dynamic/NewsList/index.js b/components/dynamic/NewsList/index.js index 453eb0ae..3a590b68 100644 --- a/components/dynamic/NewsList/index.js +++ b/components/dynamic/NewsList/index.js @@ -4,11 +4,11 @@ import { Grid } from "@rubin-epo/epo-react-lib"; import DataList from "@/dynamic/DataList"; import Tile from "@/atomic/Tile"; import { - makeDateString, makeTruncatedString, makeReleaseFeature, useGlobalData, } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; import { fallbackLng } from "@/lib/i18n/settings"; const NewsList = ({ @@ -23,7 +23,7 @@ const NewsList = ({ }) => { const { t } = useTranslation(); const localeInfo = useGlobalData("localeInfo"); - const lang = localeInfo?.language || fallbackLng; + const locale = localeInfo?.language || fallbackLng; const cols = initialLimit === 4 ? 4 : initialLimit === 3 ? 3 : 2; const canShowFeatured = initialLimit > 4; @@ -84,7 +84,7 @@ const NewsList = ({ ? postType[0].title : null } - subtitle={makeDateString(date || releaseDate, lang)} + subtitle={makeDateString(date || releaseDate, { locale })} text={makeTruncatedString(description || subtitle, 30)} title={title} titleTag={"h2"} diff --git a/components/dynamic/SearchList/index.js b/components/dynamic/SearchList/index.js index a6f3e6ff..516fbac0 100644 --- a/components/dynamic/SearchList/index.js +++ b/components/dynamic/SearchList/index.js @@ -4,10 +4,11 @@ import { useTranslation } from "react-i18next"; import { makeBreadcrumbs, makeCustomBreadcrumbs, - makeDateString, makeTruncatedString, useGlobalData, } from "@/lib/utils"; + +import { makeDateString } from "@/helpers/dates"; import Breadcrumbs from "@/components/page/Breadcrumbs"; import { Grid } from "@rubin-epo/epo-react-lib"; import DataList from "@/dynamic/DataList"; @@ -25,7 +26,7 @@ const SearchList = ({ const { t } = useTranslation(); const localeInfo = useGlobalData("localeInfo"); const rootPages = useGlobalData("rootPages"); - const lang = localeInfo?.language || fallbackLng; + const locale = localeInfo?.language || fallbackLng; const makePretitle = (entry) => { if (entry.eventType) { @@ -79,7 +80,7 @@ const SearchList = ({
{type ? `${type} ` : ``}
{entry.date - ? `${t("published")} ${makeDateString(entry.date, lang)}` + ? `${t("published")} ${makeDateString(entry.date, { locale })}` : ``}
diff --git a/components/templates/HomePage/index.js b/components/templates/HomePage/index.js index 64c6e39f..a1297cd3 100644 --- a/components/templates/HomePage/index.js +++ b/components/templates/HomePage/index.js @@ -4,7 +4,8 @@ import { useTranslation } from "react-i18next"; import ContentBlockFactory from "@/factories/ContentBlockFactory"; import Hero from "@/components/molecules/Hero"; import { Buttonish, MixedLink } from "@rubin-epo/epo-react-lib"; -import { makeDateString, makeTruncatedString } from "@/lib/utils"; +import { makeTruncatedString } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; import { SlideBlock } from "@/components/content-blocks"; import Tabs from "@/components/layout/Tabs"; import TempList from "@/components/dynamic/TempList"; diff --git a/components/templates/NewsPage/NewsArticle.js b/components/templates/NewsPage/NewsArticle.js index eee45f9f..6d0b2314 100644 --- a/components/templates/NewsPage/NewsArticle.js +++ b/components/templates/NewsPage/NewsArticle.js @@ -1,8 +1,9 @@ +"use client"; import PropTypes from "prop-types"; import Link from "next/link"; import { useTranslation } from "react-i18next"; import useResizeObserver from "use-resize-observer"; -import { useDateString } from "@/lib/utils"; +import { makeDateString } from "@/helpers/dates"; import ContentBlockFactory from "@/factories/ContentBlockFactory"; import { Container } from "@rubin-epo/epo-react-lib"; import { Share } from "@/content-blocks"; @@ -27,8 +28,11 @@ export default function NewsArticle({ data }) { releaseUrl, } = data; - const { t } = useTranslation(); - const localizedDate = useDateString(date); + const { + t, + i18n: { resolvedLanguage }, + } = useTranslation(); + const localizedDate = makeDateString(date, { locale: resolvedLanguage }); const { ref } = useResizeObserver({ box: "border-box", onResize: ({ height }) => { diff --git a/helpers/dates.ts b/helpers/dates.ts new file mode 100644 index 00000000..2e737a44 --- /dev/null +++ b/helpers/dates.ts @@ -0,0 +1,72 @@ +import { fallbackLng } from "@/lib/i18n/settings"; + +const normalizeCraftDate = (date: Date) => { + date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000); +}; + +interface DateStringOptions { + locale?: string; + isShort?: boolean; + isCraftDate?: boolean; +} + +export const makeDateString = ( + date: string, + options: DateStringOptions = {} +) => { + const { isShort = false, isCraftDate = true, locale = fallbackLng } = options; + const newDate = new Date(date); + + /** + * Craft dates are stored in UTC with the timezone applied as an hours offset + * The user's timezone offset needs to be removed to restore the original date + * + * https://craftcms.com/docs/4.x/time-fields.html#converting-from-a-date-field + */ + if (isCraftDate) { + normalizeCraftDate(newDate); + } + const localeOptions: Intl.DateTimeFormatOptions = { + year: "numeric", + month: isShort ? "short" : "long", + day: "numeric", + }; + let dateString = newDate.toLocaleString(locale, localeOptions); + isShort && (dateString = dateString.replace(",", "")); + + return dateString; +}; + +export const makeDateObject = ( + date: string, + options: DateStringOptions = {} +): Record | undefined => { + if (!date) return; + const { isShort = false, isCraftDate = true, locale = fallbackLng } = options; + const newDate = new Date(date); + + /** + * Craft dates are stored in UTC with the timezone applied as an hours offset + * The user's timezone offset needs to be removed to restore the original date + * + * https://craftcms.com/docs/4.x/time-fields.html#converting-from-a-date-field + */ + if (isCraftDate) { + normalizeCraftDate(newDate); + } + + const localeOptions: Intl.DateTimeFormatOptions = { + year: "numeric", + month: isShort ? "short" : "long", + day: "numeric", + }; + + return new Intl.DateTimeFormat(locale, localeOptions) + .formatToParts(newDate) + .filter(({ type }) => type !== "literal") + .reduce((prev, { type, value }) => { + prev[type] = value; + + return prev; + }, {}); +}; diff --git a/lib/utils.js b/lib/utils.js index 0049bf51..32f4b8aa 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -179,34 +179,6 @@ function dateWoTimezone(iso) { return new Date(iso.slice(0, -6)); } -export const useDateString = (date, options = {}) => { - const { isShort = false, isCraftDate = true } = options; - const localeInfo = useGlobalData("localeInfo"); - const locale = localeInfo.language || fallbackLng; - const newDate = new Date(date); - - /** - * Craft dates are stored in UTC with the timezone applied as an hours offset - * The user's timezone offset needs to be removed to restore the original date - * - * https://craftcms.com/docs/4.x/time-fields.html#converting-from-a-date-field - */ - if (isCraftDate) { - newDate.setTime( - newDate.getTime() + newDate.getTimezoneOffset() * 60 * 1000 - ); - } - const localeOptions = { - year: "numeric", - month: isShort ? "short" : "long", - day: "numeric", - }; - let dateString = newDate.toLocaleString(locale, localeOptions); - isShort && (dateString = dateString.replace(",", "")); - - return dateString; -}; - export const useTimeString = (iso) => { const localeInfo = useGlobalData("localeInfo"); const locale = localeInfo.language || fallbackLng; @@ -283,32 +255,6 @@ export const makeCustomBreadcrumbs = (rootPages, rootPageString) => { return customBreadcrumbs.flat(1); }; -export const makeDateString = (date, locale = fallbackLng, isShort = false) => { - const newDate = new Date(date); - const options = { - year: "numeric", - month: `${isShort ? "short" : "long"}`, - day: "numeric", - }; - let dateString = newDate.toLocaleString(locale, options); - isShort && (dateString = dateString.replace(",", "")); - return dateString; -}; - -export const makeDateObject = (date, locale = fallbackLng, isShort = false) => { - if (!date) return; - const newDate = new Date(date); - const options = { - month: `${isShort ? "short" : "long"}`, - }; - const dateObject = { - year: newDate.getFullYear(), - month: new Intl.DateTimeFormat(locale, options).format(newDate), - day: newDate.getDate(), - }; - return dateObject; -}; - export const checkIfBetweenDates = (startDate, endDate) => { if (!startDate || !endDate) { return true;