forked from nodejs/nodejs.org
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: create Common/BlogPostCard component (nodejs#6037)
* feat: create shortHumanReadableDate to parse dates into a short friendlier to read version * feat: create Common/Card component * fix: update Avatar component styles to avoid layout shift issue * fix: remove h1 tag from Common/Preview component, to avoid having more than one h1 tag in any given page * feat: create Author type, rename property to authors, and modify the code accordingly * refactor: rename Card to BlogPostCard, remove subtitle prop, and merge firstName and lastName into a fullName prop * refactor: remove shortHumanReadableDate abstraction, and add unit tests to Common/BlogPostCard
- Loading branch information
1 parent
960d704
commit 303cb62
Showing
7 changed files
with
312 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,5 +18,8 @@ | |
|
||
.avatarRoot { | ||
@apply -ml-2 | ||
h-8 | ||
w-8 | ||
flex-shrink-0 | ||
first:ml-0; | ||
} |
123 changes: 123 additions & 0 deletions
123
components/Common/BlogPostCard/__tests__/index.test.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
|
||
import BlogPostCard from '@/components/Common/BlogPostCard'; | ||
import { LocaleProvider } from '@/providers/localeProvider'; | ||
|
||
function renderBlogPostCard({ | ||
title = 'Blog post title', | ||
type = 'vulnerability', | ||
description = 'Blog post description', | ||
authors = [], | ||
date = new Date(), | ||
}) { | ||
render( | ||
<LocaleProvider> | ||
<BlogPostCard | ||
title={title} | ||
type={type} | ||
description={description} | ||
authors={authors} | ||
date={date} | ||
/> | ||
</LocaleProvider> | ||
); | ||
|
||
return { title, type, description, authors, date }; | ||
} | ||
|
||
describe('BlogPostCard', () => { | ||
describe('Rendering', () => { | ||
it('Wraps the entire card within an article', () => { | ||
renderBlogPostCard({}); | ||
|
||
expect(screen.getByRole('article')).toBeVisible(); | ||
}); | ||
|
||
it('Renders the title prop correctly', () => { | ||
const { title } = renderBlogPostCard({}); | ||
|
||
// Title from Preview component | ||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( | ||
title | ||
); | ||
|
||
// The second title should be hidden for screen-readers | ||
// to prevent them from reading it twice | ||
expect(screen.getAllByText(title)[1]).toHaveAttribute( | ||
'aria-hidden', | ||
'true' | ||
); | ||
}); | ||
|
||
it('Renders the description prop correctly', () => { | ||
const { description } = renderBlogPostCard({}); | ||
|
||
expect(screen.getByText(description)).toBeVisible(); | ||
}); | ||
|
||
it.each([ | ||
{ label: 'Vulnerabilities', type: 'vulnerability' }, | ||
{ label: 'Announcements', type: 'announcement' }, | ||
{ label: 'Releases', type: 'release' }, | ||
])( | ||
'Renders "%label" text when passing it the type "%type"', | ||
({ label, type }) => { | ||
renderBlogPostCard({ type }); | ||
|
||
expect(screen.getByText(label)).toBeVisible(); | ||
} | ||
); | ||
|
||
it('Renders all passed authors fullName(s), comma-separated', () => { | ||
const authors = [ | ||
{ fullName: 'Jane Doe', src: '' }, | ||
{ fullName: 'John Doe', src: '' }, | ||
]; | ||
|
||
renderBlogPostCard({ authors }); | ||
|
||
const fullNames = authors.reduce((prev, curr, index) => { | ||
if (index === 0) { | ||
return curr.fullName; | ||
} | ||
|
||
return `${prev}, ${curr.fullName}`; | ||
}, ''); | ||
|
||
expect(screen.getByText(fullNames)).toBeVisible(); | ||
}); | ||
|
||
it('Renders all passed authors fullName(s), comma-separated', () => { | ||
const authors = [ | ||
{ fullName: 'Jane Doe', src: '' }, | ||
{ fullName: 'John Doe', src: '' }, | ||
]; | ||
|
||
renderBlogPostCard({ authors }); | ||
|
||
const fullNames = authors.reduce((prev, curr, index) => { | ||
if (index === 0) { | ||
return curr.fullName; | ||
} | ||
|
||
return `${prev}, ${curr.fullName}`; | ||
}, ''); | ||
|
||
expect(screen.getByText(fullNames)).toBeVisible(); | ||
}); | ||
|
||
it('Renders date prop in short format', () => { | ||
const date = new Date(); | ||
|
||
renderBlogPostCard({ date }); | ||
|
||
const dateTimeFormat = new Intl.DateTimeFormat(navigator.language, { | ||
day: 'numeric', | ||
month: 'short', | ||
year: 'numeric', | ||
}); | ||
|
||
expect(screen.getByText(dateTimeFormat.format(date))).toBeVisible(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
.container { | ||
@apply max-w-full | ||
bg-white | ||
dark:bg-neutral-950; | ||
} | ||
|
||
.preview { | ||
@apply mb-6 | ||
max-w-full | ||
rounded | ||
p-4; | ||
} | ||
|
||
.subtitle { | ||
@apply mb-2 | ||
text-xs | ||
font-semibold | ||
text-green-600 | ||
dark:text-green-400; | ||
} | ||
|
||
.title { | ||
@apply mb-2 | ||
text-xl | ||
font-semibold | ||
text-neutral-900 | ||
dark:text-white; | ||
} | ||
|
||
.description { | ||
@apply mb-6 | ||
line-clamp-3 | ||
text-sm | ||
text-neutral-800 | ||
dark:text-neutral-200; | ||
} | ||
|
||
.footer { | ||
@apply flex | ||
gap-x-3; | ||
} | ||
|
||
.author { | ||
@apply text-sm | ||
font-semibold | ||
text-neutral-900 | ||
dark:text-white; | ||
} | ||
|
||
.date { | ||
@apply text-sm | ||
text-neutral-800 | ||
dark:text-neutral-200; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import type { Meta as MetaObj, StoryObj } from '@storybook/react'; | ||
|
||
import BlogPostCard from '@/components/Common/BlogPostCard'; | ||
|
||
type Story = StoryObj<typeof BlogPostCard>; | ||
type Meta = MetaObj<typeof BlogPostCard>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
title: 'Node.js March 17th Infrastructure Incident Post-mortem', | ||
type: 'vulnerability', | ||
description: | ||
'Starting on March 15th and going through to March 17th (with much of the issue being mitigated on the 16th), users were receiving intermittent 404 responses when trying to download Node.js from nodejs.org, or even accessing parts of the website.', | ||
authors: [ | ||
{ | ||
fullName: 'Hayden Bleasel', | ||
src: 'https://avatars.githubusercontent.com/u/', | ||
}, | ||
], | ||
date: new Date(), | ||
}, | ||
decorators: [ | ||
Story => ( | ||
<div className="max-w-lg"> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
}; | ||
|
||
export const MoreThanOneAuthor: Story = { | ||
...Default, | ||
args: { | ||
...Default.args, | ||
authors: [ | ||
...(Default.args?.authors ?? []), | ||
{ | ||
fullName: 'Jane Doe', | ||
src: 'https://avatars.githubusercontent.com/u/', | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
export default { component: BlogPostCard } as Meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { useMemo } from 'react'; | ||
import type { ComponentProps, FC } from 'react'; | ||
import { FormattedMessage } from 'react-intl'; | ||
|
||
import AvatarGroup from '@/components/Common/AvatarGroup'; | ||
import Preview from '@/components/Common/Preview'; | ||
|
||
import styles from './index.module.css'; | ||
|
||
const dateTimeFormat = new Intl.DateTimeFormat(navigator.language, { | ||
day: 'numeric', | ||
month: 'short', | ||
year: 'numeric', | ||
}); | ||
|
||
type Author = { | ||
fullName: string; | ||
src: string; | ||
}; | ||
|
||
type BlogPostCardProps = { | ||
title: ComponentProps<typeof Preview>['title']; | ||
type: Required<ComponentProps<typeof Preview>>['type']; | ||
description: string; | ||
authors: Author[]; | ||
date: Date; | ||
}; | ||
|
||
const BlogPostCard: FC<BlogPostCardProps> = ({ | ||
title, | ||
type, | ||
description, | ||
authors, | ||
date, | ||
}) => { | ||
const avatars = useMemo( | ||
() => | ||
authors.map(({ fullName, src }) => ({ | ||
alt: fullName, | ||
src, | ||
toString: () => fullName, | ||
})), | ||
[authors] | ||
); | ||
|
||
const formattedDate = useMemo( | ||
() => ({ | ||
ISOString: date.toISOString(), | ||
short: dateTimeFormat.format(date), | ||
}), | ||
[date] | ||
); | ||
|
||
return ( | ||
<article className={styles.container}> | ||
<Preview | ||
title={title} | ||
type={type} | ||
height="auto" | ||
className={styles.preview} | ||
/> | ||
<p className={styles.subtitle}> | ||
<FormattedMessage id={`components.common.card.${type}`} /> | ||
</p> | ||
<p aria-hidden="true" className={styles.title}> | ||
{title} | ||
</p> | ||
<p className={styles.description}>{description}</p> | ||
<footer className={styles.footer}> | ||
<AvatarGroup avatars={avatars} /> | ||
<div> | ||
<p className={styles.author}>{avatars.join(', ')}</p> | ||
<time className={styles.date} dateTime={formattedDate.ISOString}> | ||
{formattedDate.short} | ||
</time> | ||
</div> | ||
</footer> | ||
</article> | ||
); | ||
}; | ||
|
||
export default BlogPostCard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters