Skip to content

Commit

Permalink
Feat: create Common/BlogPostCard component (nodejs#6037)
Browse files Browse the repository at this point in the history
* 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
danielmarcano authored Oct 26, 2023
1 parent 960d704 commit 303cb62
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 2 deletions.
3 changes: 3 additions & 0 deletions components/Common/AvatarGroup/Avatar/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@

.avatarRoot {
@apply -ml-2
h-8
w-8
flex-shrink-0
first:ml-0;
}
123 changes: 123 additions & 0 deletions components/Common/BlogPostCard/__tests__/index.test.mjs
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();
});
});
});
54 changes: 54 additions & 0 deletions components/Common/BlogPostCard/index.module.css
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;
}
45 changes: 45 additions & 0 deletions components/Common/BlogPostCard/index.stories.tsx
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;
82 changes: 82 additions & 0 deletions components/Common/BlogPostCard/index.tsx
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;
2 changes: 1 addition & 1 deletion components/Common/Preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const Preview: FC<PreviewProps> = ({
src={`${basePath}/static/images/logos/js-white.svg`}
alt="Node.js"
/>
<h1>{title}</h1>
<h2>{title}</h2>
</div>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,8 @@
"components.common.pagination.nextAriaLabel": "Next page",
"components.common.pagination.defaultLabel": "Pagination",
"components.common.pagination.pageLabel": "Go to page {pageNumber}",
"components.common.languageDropdown.label": "Choose Language"
"components.common.languageDropdown.label": "Choose Language",
"components.common.card.announcement": "Announcements",
"components.common.card.release": "Releases",
"components.common.card.vulnerability": "Vulnerabilities"
}

0 comments on commit 303cb62

Please sign in to comment.