Skip to content

LocalDate and NoSSR components to render localized dates only on client #1831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "PORT=${PORT:-8062} next dev",
"dev": "PORT=${PORT:-8062} TZ=UTC next dev",
"build": "next build",
"build:no-lint": "next build --no-lint",
"start": "next start",
"start": "TZ=UTC next start",
"lint": "next lint"
},
"dependencies": {
Expand Down
20 changes: 10 additions & 10 deletions frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
NewsEventsListFeedTypeEnum,
} from "api/hooks/newsEvents"
import type { NewsFeedItem, EventFeedItem } from "api/v0"
import { formatDate } from "ol-utilities"
import { LocalDate } from "ol-utilities"
import { RiArrowRightSLine } from "@remixicon/react"
import Link from "next/link"

Expand Down Expand Up @@ -196,7 +196,7 @@ const Story: React.FC<{ item: NewsFeedItem; mobile: boolean }> = ({
{item.title}
</Card.Title>
<Card.Footer>
Published: {formatDate(item.news_details?.publish_date)}
Published: <LocalDate date={item.news_details?.publish_date} />
</Card.Footer>
</StoryCard>
)
Expand Down Expand Up @@ -226,16 +226,16 @@ const NewsEventsSection: React.FC = () => {
<Card.Content>
<EventDate>
<EventDay>
{formatDate(
(item as EventFeedItem).event_details?.event_datetime,
"D",
)}
<LocalDate
date={(item as EventFeedItem).event_details?.event_datetime}
format="D"
/>
</EventDay>
<EventMonth>
{formatDate(
(item as EventFeedItem).event_details?.event_datetime,
"MMM",
)}
<LocalDate
date={(item as EventFeedItem).event_details?.event_datetime}
format="MMM"
/>
</EventMonth>
</EventDate>
<Link href={item.url} data-card-link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@remixicon/react"
import { LearningResource } from "api"
import {
formatDate,
LocalDate,
getReadableResourceType,
DEFAULT_RESOURCE_IMG,
getLearningResourcePrices,
Expand Down Expand Up @@ -149,7 +149,8 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({
const format = size === "small" ? "MMM DD, YYYY" : "MMMM DD, YYYY"
const formatted = anytime
? "Anytime"
: startDate && formatDate(startDate, format)
: startDate && <LocalDate date={startDate} format={format} />

if (!formatted) return null

const showLabel = size !== "small" || anytime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@remixicon/react"
import { ResourceTypeEnum, LearningResource } from "api"
import {
formatDate,
LocalDate,
getReadableResourceType,
DEFAULT_RESOURCE_IMG,
pluralize,
Expand Down Expand Up @@ -151,7 +151,7 @@ export const StartDate: React.FC<{ resource: LearningResource }> = ({
const startDate = getResourceDate(resource)
const formatted = anytime
? "Anytime"
: startDate && formatDate(startDate, "MMMM DD, YYYY")
: startDate && <LocalDate date={startDate} format="MMMM DD, YYYY" />
if (!formatted) return null

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getDisplayPrice,
getRunPrices,
showStartAnytime,
NoSSR,
} from "ol-utilities"

const DifferingRuns = styled.div({
Expand Down Expand Up @@ -103,7 +104,9 @@ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({
</DifferingRunHeader>
{resource.runs?.map((run, index) => (
<DifferingRun key={index}>
<DateData>{formatRunDate(run, asTaughtIn)}</DateData>
<DateData>
<NoSSR>{formatRunDate(run, asTaughtIn)}</NoSSR>
</DateData>
{run.resource_prices && (
<PriceData>
<span>{getDisplayPrice(getRunPrices(run)["course"])}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
formatRunDate,
getLearningResourcePrices,
showStartAnytime,
NoSSR,
} from "ol-utilities"
import { theme } from "../ThemeProvider/ThemeProvider"
import DifferingRunsTable from "./DifferingRunsTable"
Expand Down Expand Up @@ -255,7 +256,11 @@ const INFO_ITEMS: InfoItemConfig = [
const totalDatesWithRuns =
resource.runs?.filter((run) => run.start_date !== null).length || 0
if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) {
return <RunDates resource={resource} />
return (
<NoSSR>
<RunDates resource={resource} />
</NoSSR>
)
} else return null
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ButtonLink } from "../Button/Button"
import type { LearningResource, LearningResourceRun } from "api"
import { ResourceTypeEnum, PlatformEnum } from "api"
import {
NoSSR,
formatDate,
capitalize,
DEFAULT_RESOURCE_IMG,
Expand Down Expand Up @@ -299,7 +300,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => {
return (
<Description
/**
* Resource descriptions can contain HTML. They are santiized on the
* Resource descriptions can contain HTML. They are sanitized on the
* backend during ETL. This is safe to render.
*/
dangerouslySetInnerHTML={{ __html: resource.description || "" }}
Expand Down Expand Up @@ -384,7 +385,7 @@ const LearningResourceExpandedV1: React.FC<LearningResourceExpandedV1Props> = ({
.map((run) => {
return {
value: run.id.toString(),
label: formatRunDate(run, asTaughtIn),
label: <NoSSR>{formatRunDate(run, asTaughtIn)}</NoSSR>,
}
}) ?? []

Expand Down Expand Up @@ -415,7 +416,7 @@ const LearningResourceExpandedV1: React.FC<LearningResourceExpandedV1Props> = ({
return (
<DateSingle>
<DateLabel>{label}</DateLabel>
{formatted ?? ""}
<NoSSR>{formatted ?? ""}</NoSSR>
</DateSingle>
)
}
Expand Down
20 changes: 20 additions & 0 deletions frontends/ol-utilities/src/date/LocalDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react"
import { NoSSR } from "../ssr/NoSSR"
import { formatDate } from "./format"

type LocalDateProps = {
date?: string | Date | null
/**
* A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/
*/
format?: string
}

/* Component to render dates only on the client as these are displayed
* according to the user's locale (generally, not all Moment.js format tokens
* are localized) causing an error due to hydration mismatch.
*/
export const LocalDate = ({ date, format = "MMM D, YYYY" }: LocalDateProps) => {
if (!date) return null
return <NoSSR>{formatDate(date, format)}</NoSSR>
}
4 changes: 3 additions & 1 deletion frontends/ol-utilities/src/date/format.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import moment from "moment"

/* Instances must be wrapped in <NoSSR> to avoid SSR hydration mismatches.
*/
export const formatDate = (
/**
* Date string or date.
*/
date: string | Date,
/**
* A momentjs format string. See https://momentjs.com/docs/#/displaying/format/
* A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/
*/
format = "MMM D, YYYY",
) => {
Expand Down
2 changes: 2 additions & 0 deletions frontends/ol-utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export * from "./styles"

export * from "./date/format"
export * from "./date/LocalDate"
export * from "./learning-resources/learning-resources"
export * from "./learning-resources/pricing"
export * from "./strings/html"
Expand All @@ -14,3 +15,4 @@ export * from "./hooks"
export * from "./querystrings"
export * from "./lib"
export * from "./images/backgroundImages"
export * from "./ssr/NoSSR"
16 changes: 16 additions & 0 deletions frontends/ol-utilities/src/ssr/NoSSR.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { useState, useEffect, ReactNode } from "react"

type NoSSRProps = {
children: ReactNode
onSSR?: ReactNode
}

export const NoSSR: React.FC<NoSSRProps> = ({ children, onSSR = null }) => {
const [isClient, setClient] = useState(false)

useEffect(() => {
setClient(true)
}, [])

return isClient ? children : onSSR
}