Skip to content
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

fix: improve date parsing for collection items #732

Merged
merged 6 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions apps/studio/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const MockDateDecorator: Decorator<Args> = (story, { parameters }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockdate.set(parameters.mockdate)

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const mockedDate = format(parameters.mockdate, "dd-mm-yyyy HH:mma")

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"@trpc/react-query": "10.45.0",
"@trpc/server": "10.45.0",
"ajv": "^8.16.0",
"date-fns": "^3.6.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.1.3",
"flat": "^6.0.1",
"fuzzysort": "^2.0.4",
Expand Down
23 changes: 21 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"@govtechsg/sgds-react": "^2.5.1",
"@headlessui/react": "^2.1.2",
"@sinclair/typebox": "^0.33.12",
"dayjs": "^1.11.13",
"date-fns": "^4.1.0",
"interweave": "^13.1.0",
"interweave-ssr": "^2.0.0",
"isomorphic-dompurify": "^2.12.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type {
CollectionPageSchemaType,
IsomerSitemap,
IsomerSiteProps,
} from "~/engine"
import type { CollectionPageSchemaType, IsomerSiteProps } from "~/engine"
import type { CollectionCardProps } from "~/interfaces"
import { getBreadcrumbFromSiteMap, getSitemapAsArray } from "~/utils"
import { Skeleton } from "../Skeleton"
Expand Down Expand Up @@ -49,13 +45,8 @@ const getCollectionItems = (
item.layout === "article",
)
.map((item) => {
const date = new Date(item.date || item.lastModified)
const lastUpdated =
date.getDate().toString().padStart(2, "0") +
" " +
date.toLocaleString("default", { month: "long" }) +
" " +
date.getFullYear()
const lastUpdated = item.date || item.lastModified
const date = new Date(lastUpdated)

const baseItem = {
type: "collectionCard" as const,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AppliedFilter, Filter as FilterType } from "../../types/Filter"
import type { CollectionCardProps } from "~/interfaces"
import { getParsedDate } from "~/utils"

export const getAvailableFilters = (
items: CollectionCardProps[],
Expand All @@ -25,7 +26,7 @@ export const getAvailableFilters = (

// Step 3: Get all available years
if (lastUpdated) {
const year = new Date(lastUpdated).getFullYear().toString()
const year = getParsedDate(lastUpdated).getFullYear().toString()
if (year in years && years[year]) {
years[year] += 1
} else {
Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Tagged } from "type-fest"

export type ValueOf<T> = T[keyof T]

// This is a branded type for a formatted date string using getFormattedDate
export type FormattedDate = Tagged<string, "FormattedDate">

// This is the Next.js Link component that resembles the HTML anchor tag
export type LinkComponentType = any

Expand Down
22 changes: 12 additions & 10 deletions packages/components/src/utils/getFormattedDate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import { format } from "date-fns"

dayjs.extend(customParseFormat)
import type { FormattedDate } from "~/types"
import { getParsedDate } from "./getParsedDate"

// Standardise the format of dates displayed on the site
export const getFormattedDate = (date: string) =>
dayjs(date, [
"DD/MM/YYYY",
"D MMM YYYY",
"DD MMM YYYY",
"YYYY-MM-DDTHH:mm:ss.SSSZ",
]).format("D MMMM YYYY")
export const getFormattedDate = (dateString?: string): FormattedDate => {
if (dateString === undefined) {
return format(new Date(), "d MMMM yyyy") as FormattedDate
}

const date = getParsedDate(dateString)

return format(date, "d MMMM yyyy") as FormattedDate
}
39 changes: 39 additions & 0 deletions packages/components/src/utils/getParsedDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { isMatch, parse } from "date-fns"

const SUPPORTED_DATE_FORMATS = [
"dd/MM/yyyy",
"d MMM yyyy",
"d MMMM yyyy",
"dd MMM yyyy",
"dd MMMM yyyy",
"yyyy-MM-dd",
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
]

export const getParsedDate = (dateString: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually the flow i had in mind was reduce(function, new Date()

then in our reducer function:

if (isMatch(date)) { // parse } 

and we can put supported date formats in increasing order of priority, so that the code looks cleaner overall. not a big deal though, just that this was not quite what i had in mind

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I a bit confused, I think that's what I'm doing right now? The reducer function is indeed going by priority (from first to last element) and it starts with undefined to differentiate between whether the date string has been parsed or not. If parsed then the type is of Date, otherwise it will remain as undefined.

try {
const parsedDate = SUPPORTED_DATE_FORMATS.reduce<Date | undefined>(
(acc, format) => {
if (acc) {
// Date has already been parsed by an earlier format
return acc
}

if (isMatch(dateString, format)) {
return parse(dateString, format, new Date())
}
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should shift the try/catch here since this is the only thing that can fail

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 715e22c.


return acc
},
undefined,
)

if (parsedDate) {
return parsedDate
}

return new Date()
} catch (e) {
return new Date()
}
}
1 change: 1 addition & 0 deletions packages/components/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { getBreadcrumbFromSiteMap } from "./getBreadcrumbFromSiteMap"
export { getDigestFromText } from "./getDigestFromText"
export { getFormattedDate } from "./getFormattedDate"
export { getParsedDate } from "./getParsedDate"
export { getRandomNumberBetIntervals } from "./getRandomNumber"
export { getReferenceLinkHref } from "./getReferenceLinkHref"
export { getSanitizedIframeWithTitle } from "./getSanitizedIframeWithTitle"
Expand Down
Loading