Skip to content

Commit b4ccd6d

Browse files
committed
LocalDate and NoSSR components to render localized dates only on client
1 parent 7f2df1f commit b4ccd6d

File tree

11 files changed

+72
-22
lines changed

11 files changed

+72
-22
lines changed

frontends/main/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "PORT=${PORT:-8062} next dev",
6+
"dev": "PORT=${PORT:-8062} TZ=UTC next dev",
77
"build": "next build",
88
"build:no-lint": "next build --no-lint",
9-
"start": "next start",
9+
"start": "TZ=UTC next start",
1010
"lint": "next lint"
1111
},
1212
"dependencies": {

frontends/main/src/app-pages/HomePage/NewsEventsSection.tsx

+10-10
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
NewsEventsListFeedTypeEnum,
1414
} from "api/hooks/newsEvents"
1515
import type { NewsFeedItem, EventFeedItem } from "api/v0"
16-
import { formatDate } from "ol-utilities"
16+
import { LocalDate } from "ol-utilities"
1717
import { RiArrowRightSLine } from "@remixicon/react"
1818
import Link from "next/link"
1919

@@ -196,7 +196,7 @@ const Story: React.FC<{ item: NewsFeedItem; mobile: boolean }> = ({
196196
{item.title}
197197
</Card.Title>
198198
<Card.Footer>
199-
Published: {formatDate(item.news_details?.publish_date)}
199+
Published: <LocalDate date={item.news_details?.publish_date} />
200200
</Card.Footer>
201201
</StoryCard>
202202
)
@@ -226,16 +226,16 @@ const NewsEventsSection: React.FC = () => {
226226
<Card.Content>
227227
<EventDate>
228228
<EventDay>
229-
{formatDate(
230-
(item as EventFeedItem).event_details?.event_datetime,
231-
"D",
232-
)}
229+
<LocalDate
230+
date={(item as EventFeedItem).event_details?.event_datetime}
231+
format="D"
232+
/>
233233
</EventDay>
234234
<EventMonth>
235-
{formatDate(
236-
(item as EventFeedItem).event_details?.event_datetime,
237-
"MMM",
238-
)}
235+
<LocalDate
236+
date={(item as EventFeedItem).event_details?.event_datetime}
237+
format="MMM"
238+
/>
239239
</EventMonth>
240240
</EventDate>
241241
<Link href={item.url} data-card-link>

frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "@remixicon/react"
1010
import { LearningResource } from "api"
1111
import {
12-
formatDate,
12+
LocalDate,
1313
getReadableResourceType,
1414
DEFAULT_RESOURCE_IMG,
1515
getLearningResourcePrices,
@@ -149,7 +149,8 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({
149149
const format = size === "small" ? "MMM DD, YYYY" : "MMMM DD, YYYY"
150150
const formatted = anytime
151151
? "Anytime"
152-
: startDate && formatDate(startDate, format)
152+
: startDate && <LocalDate date={startDate} format={format} />
153+
153154
if (!formatted) return null
154155

155156
const showLabel = size !== "small" || anytime

frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "@remixicon/react"
1010
import { ResourceTypeEnum, LearningResource } from "api"
1111
import {
12-
formatDate,
12+
LocalDate,
1313
getReadableResourceType,
1414
DEFAULT_RESOURCE_IMG,
1515
pluralize,
@@ -151,7 +151,7 @@ export const StartDate: React.FC<{ resource: LearningResource }> = ({
151151
const startDate = getResourceDate(resource)
152152
const formatted = anytime
153153
? "Anytime"
154-
: startDate && formatDate(startDate, "MMMM DD, YYYY")
154+
: startDate && <LocalDate date={startDate} format="MMMM DD, YYYY" />
155155
if (!formatted) return null
156156

157157
return (

frontends/ol-components/src/components/LearningResourceExpanded/DifferingRunsTable.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getDisplayPrice,
99
getRunPrices,
1010
showStartAnytime,
11+
NoSSR,
1112
} from "ol-utilities"
1213

1314
const DifferingRuns = styled.div({
@@ -103,7 +104,9 @@ const DifferingRunsTable: React.FC<{ resource: LearningResource }> = ({
103104
</DifferingRunHeader>
104105
{resource.runs?.map((run, index) => (
105106
<DifferingRun key={index}>
106-
<DateData>{formatRunDate(run, asTaughtIn)}</DateData>
107+
<DateData>
108+
<NoSSR>{formatRunDate(run, asTaughtIn)}</NoSSR>
109+
</DateData>
107110
{run.resource_prices && (
108111
<PriceData>
109112
<span>{getDisplayPrice(getRunPrices(run)["course"])}</span>

frontends/ol-components/src/components/LearningResourceExpanded/InfoSectionV2.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
formatRunDate,
2525
getLearningResourcePrices,
2626
showStartAnytime,
27+
NoSSR,
2728
} from "ol-utilities"
2829
import { theme } from "../ThemeProvider/ThemeProvider"
2930
import DifferingRunsTable from "./DifferingRunsTable"
@@ -255,7 +256,11 @@ const INFO_ITEMS: InfoItemConfig = [
255256
const totalDatesWithRuns =
256257
resource.runs?.filter((run) => run.start_date !== null).length || 0
257258
if (allRunsAreIdentical(resource) && totalDatesWithRuns > 0) {
258-
return <RunDates resource={resource} />
259+
return (
260+
<NoSSR>
261+
<RunDates resource={resource} />
262+
</NoSSR>
263+
)
259264
} else return null
260265
},
261266
},

frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpandedV1.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ButtonLink } from "../Button/Button"
77
import type { LearningResource, LearningResourceRun } from "api"
88
import { ResourceTypeEnum, PlatformEnum } from "api"
99
import {
10+
NoSSR,
1011
formatDate,
1112
capitalize,
1213
DEFAULT_RESOURCE_IMG,
@@ -299,7 +300,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => {
299300
return (
300301
<Description
301302
/**
302-
* Resource descriptions can contain HTML. They are santiized on the
303+
* Resource descriptions can contain HTML. They are sanitized on the
303304
* backend during ETL. This is safe to render.
304305
*/
305306
dangerouslySetInnerHTML={{ __html: resource.description || "" }}
@@ -384,7 +385,7 @@ const LearningResourceExpandedV1: React.FC<LearningResourceExpandedV1Props> = ({
384385
.map((run) => {
385386
return {
386387
value: run.id.toString(),
387-
label: formatRunDate(run, asTaughtIn),
388+
label: <NoSSR>{formatRunDate(run, asTaughtIn)}</NoSSR>,
388389
}
389390
}) ?? []
390391

@@ -415,7 +416,7 @@ const LearningResourceExpandedV1: React.FC<LearningResourceExpandedV1Props> = ({
415416
return (
416417
<DateSingle>
417418
<DateLabel>{label}</DateLabel>
418-
{formatted ?? ""}
419+
<NoSSR>{formatted ?? ""}</NoSSR>
419420
</DateSingle>
420421
)
421422
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react"
2+
import { NoSSR } from "../ssr/NoSSR"
3+
import { formatDate } from "./format"
4+
5+
type LocalDateProps = {
6+
date?: string | Date | null
7+
/**
8+
* A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/
9+
*/
10+
format?: string
11+
}
12+
13+
/* Component to render dates only on the client as these are displayed
14+
* according to the user's locale (generally, not all Moment.js format tokens
15+
* are localized) causing an error due to hydration mismatch.
16+
*/
17+
export const LocalDate = ({ date, format = "MMM D, YYYY" }: LocalDateProps) => {
18+
if (!date) return null
19+
return <NoSSR>{formatDate(date, format)}</NoSSR>
20+
}

frontends/ol-utilities/src/date/format.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import moment from "moment"
22

3+
/* Instances must be wrapped in <NoSSR> to avoid SSR hydration mismatches.
4+
*/
35
export const formatDate = (
46
/**
57
* Date string or date.
68
*/
79
date: string | Date,
810
/**
9-
* A momentjs format string. See https://momentjs.com/docs/#/displaying/format/
11+
* A Moment.js format string. See https://momentjs.com/docs/#/displaying/format/
1012
*/
1113
format = "MMM D, YYYY",
1214
) => {

frontends/ol-utilities/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
export * from "./styles"
77

88
export * from "./date/format"
9+
export * from "./date/LocalDate"
910
export * from "./learning-resources/learning-resources"
1011
export * from "./learning-resources/pricing"
1112
export * from "./strings/html"
@@ -14,3 +15,4 @@ export * from "./hooks"
1415
export * from "./querystrings"
1516
export * from "./lib"
1617
export * from "./images/backgroundImages"
18+
export * from "./ssr/NoSSR"
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React, { useState, useEffect, ReactNode } from "react"
2+
3+
type NoSSRProps = {
4+
children: ReactNode
5+
onSSR?: ReactNode
6+
}
7+
8+
export const NoSSR: React.FC<NoSSRProps> = ({ children, onSSR = <></> }) => {
9+
const [isClient, setClient] = useState(false)
10+
11+
useEffect(() => {
12+
setClient(true)
13+
}, [])
14+
15+
return <>{isClient ? children : onSSR}</>
16+
}

0 commit comments

Comments
 (0)