1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 14:08:29 -04:00

dayjs ➡️ date-fns

This commit is contained in:
Jake Jarvis 2025-03-21 13:20:42 -04:00
parent 9fd3de8569
commit 8890c1d08d
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
9 changed files with 57 additions and 60 deletions

View File

@ -87,7 +87,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<div className={styles.metaItem}> <div className={styles.metaItem}>
<Link href={`/notes/${frontmatter.slug}` as Route} plain className={styles.metaLink}> <Link href={`/notes/${frontmatter.slug}` as Route} plain className={styles.metaLink}>
<CalendarIcon size="1.2em" className={styles.metaIcon} /> <CalendarIcon size="1.2em" className={styles.metaIcon} />
<Time date={frontmatter.date} format="MMMM D, YYYY" /> <Time date={frontmatter.date} format="MMMM d, y" />
</Link> </Link>
</div> </div>

View File

@ -38,7 +38,7 @@ const Page = async () => {
<ul className={styles.list}> <ul className={styles.list}>
{posts.map(({ slug, date, title, htmlTitle }) => ( {posts.map(({ slug, date, title, htmlTitle }) => (
<li className={styles.post} key={slug}> <li className={styles.post} key={slug}>
<Time date={date} format="MMM D" className={styles.postDate} /> <Time date={date} format="MMM d" className={styles.postDate} />
<span> <span>
<Link href={`/notes/${slug}` as Route} dangerouslySetInnerHTML={{ __html: htmlTitle || title }} /> <Link href={`/notes/${slug}` as Route} dangerouslySetInnerHTML={{ __html: htmlTitle || title }} />
</span> </span>

View File

@ -152,9 +152,8 @@ const Page = async () => {
</div> </div>
)} )}
{/* only use relative "time ago" on client side, since it'll be outdated via SSG and cause hydration errors */}
<div className={styles.metaItem}> <div className={styles.metaItem}>
<RelativeTime date={repo.updatedAt} verb="Updated" staticFormat="MMM D, YYYY" /> Updated <RelativeTime date={repo.updatedAt} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,24 +1,31 @@
"use client"; "use client";
import { useHasMounted } from "../../hooks"; import { useHasMounted } from "../../hooks";
import { formatDate, formatTimeAgo } from "../../lib/helpers/format-date"; import { format, formatISO, formatDistanceToNowStrict } from "date-fns";
import { enUS } from "date-fns/locale";
import { tz } from "@date-fns/tz";
import { utc } from "@date-fns/utc";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
export type RelativeTimeProps = ComponentPropsWithoutRef<"time"> & { export type RelativeTimeProps = ComponentPropsWithoutRef<"time"> & {
date: string | number | Date; date: string;
verb?: string; // optional "Updated", "Published", "Created", etc.
staticFormat?: string; // format for the placeholder/fallback before client-side renders the relative time
}; };
const RelativeTime = ({ date, verb, staticFormat, ...rest }: RelativeTimeProps) => { const RelativeTime = ({ date, ...rest }: RelativeTimeProps) => {
// play nice with SSR -- only use relative time on the client, since it'll quickly become outdated on the server and // play nice with SSR -- only use relative time on the client, since it'll quickly become outdated on the server and
// cause a react hydration mismatch error. // cause a react hydration mismatch error.
const hasMounted = useHasMounted(); const hasMounted = useHasMounted();
return ( return (
<time dateTime={formatDate(date)} title={formatDate(date, "MMM D, YYYY, h:mm A z")} {...rest}> <time
{verb && `${verb} `} dateTime={formatISO(date, { in: utc })}
{hasMounted ? formatTimeAgo(date, { suffix: true }) : `on ${formatDate(date, staticFormat)}`} title={format(date, "MMM d, y, h:mm a O", { in: tz(config.timeZone), locale: enUS })}
{...rest}
>
{hasMounted
? formatDistanceToNowStrict(date, { locale: enUS, addSuffix: true })
: `on ${format(date, "MMM d, y", { in: tz(config.timeZone), locale: enUS })}`}
</time> </time>
); );
}; };

View File

@ -1,15 +1,23 @@
import { formatDate } from "../../lib/helpers/format-date"; import { format, formatISO } from "date-fns";
import { enUS } from "date-fns/locale";
import { tz } from "@date-fns/tz";
import { utc } from "@date-fns/utc";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
export type TimeProps = ComponentPropsWithoutRef<"time"> & { export type TimeProps = ComponentPropsWithoutRef<"time"> & {
date: string | number | Date; date: string;
format?: string; format?: string;
}; };
const Time = ({ date, format = "MMM D", ...rest }: TimeProps) => { const Time = ({ date, format: formatStr = "PPpp", ...rest }: TimeProps) => {
return ( return (
<time dateTime={formatDate(date)} title={formatDate(date, "MMM D, YYYY, h:mm A z")} {...rest}> <time
{formatDate(date, format)} dateTime={formatISO(date, { in: utc })}
title={format(date, "MMM d, y, h:mm a O", { in: tz(config.timeZone), locale: enUS })}
{...rest}
>
{format(date, formatStr, { in: tz(config.timeZone), locale: enUS })}
</time> </time>
); );
}; };

View File

@ -1,34 +0,0 @@
import dayjs from "dayjs";
import dayjsUtc from "dayjs/plugin/utc";
import dayjsTimezone from "dayjs/plugin/timezone";
import dayjsRelativeTime from "dayjs/plugin/relativeTime";
import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat";
import dayjsAdvancedFormat from "dayjs/plugin/advancedFormat";
import "dayjs/locale/en";
import * as config from "../config";
const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => {
// plugins
dayjs.extend(dayjsUtc);
dayjs.extend(dayjsTimezone);
dayjs.extend(dayjsRelativeTime);
dayjs.extend(dayjsLocalizedFormat);
dayjs.extend(dayjsAdvancedFormat);
return dayjs(date).locale("en").tz(config.timeZone).clone();
};
// simple wrapper around dayjs.format() to normalize timezone across the site, both server and client side, to prevent
// hydration errors by returning an instance of dayjs with these defaults set.
// date defaults to now, format defaults to ISO 8601 (e.g. 2022-04-07T21:53:33-04:00)
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string): string => {
return IsomorphicDayJs(date).format(formatStr);
};
// returns the human-friendly difference between now and given date (e.g. "5 minutes", "9 months", etc.)
// set `{ suffix: true }` to include the "... ago" or "in ..." for past/future
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }): string => {
return IsomorphicDayJs().isBefore(date)
? IsomorphicDayJs(date).toNow(!options?.suffix)
: IsomorphicDayJs(date).fromNow(!options?.suffix);
};

View File

@ -4,7 +4,6 @@ import glob from "fast-glob";
import { unified } from "unified"; import { unified } from "unified";
import { remarkHtml, remarkParse, remarkSmartypants } from "./remark-rehype-plugins"; import { remarkHtml, remarkParse, remarkSmartypants } from "./remark-rehype-plugins";
import { decode } from "html-entities"; import { decode } from "html-entities";
import { formatDate } from "./format-date";
import { BASE_URL, POSTS_DIR } from "../config/constants"; import { BASE_URL, POSTS_DIR } from "../config/constants";
export type FrontMatter = { export type FrontMatter = {
@ -48,7 +47,7 @@ export const getFrontMatter = async (slug: string): Promise<FrontMatter> => {
// stylized title with limited html tags: // stylized title with limited html tags:
htmlTitle, htmlTitle,
slug, slug,
date: formatDate(frontmatter.date), // validate/normalize the date string provided from front matter date: new Date(frontmatter.date).toISOString(), // validate/normalize the date string provided from front matter
permalink: `${BASE_URL}/${POSTS_DIR}/${slug}`, permalink: `${BASE_URL}/${POSTS_DIR}/${slug}`,
}; };
}; };

View File

@ -18,6 +18,8 @@
"postinstall": "prisma generate --no-hints" "postinstall": "prisma generate --no-hints"
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@emotion/hash": "^0.9.2", "@emotion/hash": "^0.9.2",
"@giscus/react": "^3.1.0", "@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
@ -31,7 +33,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comma-number": "^2.1.0", "comma-number": "^2.1.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13", "date-fns": "^4.1.0",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"feed": "^4.2.2", "feed": "^4.2.2",
"geist": "^1.3.1", "geist": "^1.3.1",

28
pnpm-lock.yaml generated
View File

@ -8,6 +8,12 @@ importers:
.: .:
dependencies: dependencies:
'@date-fns/tz':
specifier: ^1.2.0
version: 1.2.0
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
'@emotion/hash': '@emotion/hash':
specifier: ^0.9.2 specifier: ^0.9.2
version: 0.9.2 version: 0.9.2
@ -47,9 +53,9 @@ importers:
copy-to-clipboard: copy-to-clipboard:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
dayjs: date-fns:
specifier: ^1.11.13 specifier: ^4.1.0
version: 1.11.13 version: 4.1.0
fast-glob: fast-glob:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
@ -389,6 +395,12 @@ packages:
peerDependencies: peerDependencies:
postcss-selector-parser: ^7.0.0 postcss-selector-parser: ^7.0.0
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
'@date-fns/utc@2.1.0':
resolution: {integrity: sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA==}
'@discoveryjs/json-ext@0.5.7': '@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -1515,8 +1527,8 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dayjs@1.11.13: date-fns@4.1.0:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debounce@1.2.1: debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
@ -4067,6 +4079,10 @@ snapshots:
dependencies: dependencies:
postcss-selector-parser: 7.1.0 postcss-selector-parser: 7.1.0
'@date-fns/tz@1.2.0': {}
'@date-fns/utc@2.1.0': {}
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@dual-bundle/import-meta-resolve@4.1.0': {} '@dual-bundle/import-meta-resolve@4.1.0': {}
@ -5162,7 +5178,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.2 is-data-view: 1.0.2
dayjs@1.11.13: {} date-fns@4.1.0: {}
debounce@1.2.1: {} debounce@1.2.1: {}