aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-13 19:34:49 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-13 19:34:49 +0200
commit872b0c172a38db4f440dc6044eb1d5725c03abb1 (patch)
tree712c7ad605fb21a01be0acc5faa081051955d60a
parent47e35fcd7c2c346f4799630bf6521d6a4bf49e85 (diff)
chore: add a CardsList component
-rw-r--r--src/components/organisms/layout/cards-list.module.scss27
-rw-r--r--src/components/organisms/layout/cards-list.stories.tsx105
-rw-r--r--src/components/organisms/layout/cards-list.test.tsx61
-rw-r--r--src/components/organisms/layout/cards-list.tsx80
4 files changed, 273 insertions, 0 deletions
diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss
new file mode 100644
index 0000000..9fe428c
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.module.scss
@@ -0,0 +1,27 @@
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ --card-width: 30ch;
+
+ display: grid;
+ grid-template-columns: repeat(
+ auto-fit,
+ min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width))
+ );
+ gap: var(--spacing-sm);
+ place-content: center;
+ align-items: stretch;
+ justify-items: stretch;
+
+ &--ordered {
+ @extend %reset-ordered-list;
+ }
+
+ &--unordered {
+ @extend %reset-list;
+ }
+}
+
+.card {
+ height: 100%;
+}
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
new file mode 100644
index 0000000..5182808
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -0,0 +1,105 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CardsListComponent, { type CardsListItem } from './cards-list';
+
+export default {
+ title: 'Organisms/Layout',
+ component: CardsListComponent,
+ args: {
+ kind: 'unordered',
+ },
+ argTypes: {
+ items: {
+ description: 'The cards data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind.',
+ options: ['ordered', 'unordered'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'unordered' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ },
+ description: 'The heading level for each card.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CardsListComponent>;
+
+const Template: ComponentStory<typeof CardsListComponent> = (args) => (
+ <CardsListComponent {...args} />
+);
+
+const items: CardsListItem[] = [
+ {
+ id: 'card-1',
+ cover: {
+ alt: 'card 1 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [
+ { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] },
+ ],
+ tagline: 'Molestias ut error',
+ title: 'Et alias omnis',
+ url: '#',
+ },
+ {
+ id: 'card-2',
+ cover: {
+ alt: 'card 2 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }],
+ tagline: 'Quod vel accusamus',
+ title: 'Laboriosam doloremque mollitia',
+ url: '#',
+ },
+ {
+ id: 'card-3',
+ cover: {
+ alt: 'card 3 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [
+ {
+ id: 'meta-1',
+ term: 'Omnis',
+ value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
+ },
+ ],
+ tagline: 'Quo error eum',
+ title: 'Magni rem nulla',
+ url: '#',
+ },
+];
+
+export const CardsList = Template.bind({});
+CardsList.args = {
+ items,
+ titleLevel: 2,
+};
diff --git a/src/components/organisms/layout/cards-list.test.tsx b/src/components/organisms/layout/cards-list.test.tsx
new file mode 100644
index 0000000..2df3f59
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.test.tsx
@@ -0,0 +1,61 @@
+import { render, screen } from '@test-utils';
+import CardsList from './cards-list';
+
+const items = [
+ {
+ id: 'card-1',
+ cover: {
+ alt: 'card 1 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [
+ { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] },
+ ],
+ tagline: 'Molestias ut error',
+ title: 'Et alias omnis',
+ url: '#',
+ },
+ {
+ id: 'card-2',
+ cover: {
+ alt: 'card 2 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }],
+ tagline: 'Quod vel accusamus',
+ title: 'Laboriosam doloremque mollitia',
+ url: '#',
+ },
+ {
+ id: 'card-3',
+ cover: {
+ alt: 'card 3 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: [
+ {
+ id: 'meta-1',
+ term: 'Omnis',
+ value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
+ },
+ ],
+ tagline: 'Quo error eum',
+ title: 'Magni rem nulla',
+ url: '#',
+ },
+];
+
+describe('CardsList', () => {
+ it('renders a list of cards', () => {
+ render(<CardsList items={items} titleLevel={2} />);
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(
+ items.length
+ );
+ });
+});
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
new file mode 100644
index 0000000..a53df0d
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -0,0 +1,80 @@
+import List, {
+ type ListItem,
+ type ListProps,
+} from '@components/atoms/lists/list';
+import Card, { type CardProps } from '@components/molecules/layout/card';
+import { VFC } from 'react';
+import styles from './cards-list.module.scss';
+
+export type CardsListItem = Omit<
+ CardProps,
+ 'className' | 'coverFit' | 'titleLevel'
+> & {
+ id: string;
+};
+
+export type CardsListProps = {
+ /**
+ * The cover fit.
+ */
+ coverFit?: CardProps['coverFit'];
+ /**
+ * The cards data.
+ */
+ items: CardsListItem[];
+ /**
+ * The list kind. Either ordered or unordered.
+ */
+ kind?: ListProps['kind'];
+ /**
+ * The title level (hn).
+ */
+ titleLevel: CardProps['titleLevel'];
+};
+
+/**
+ * CardsList component
+ *
+ * Return a list of Card components.
+ */
+const CardsList: VFC<CardsListProps> = ({
+ coverFit,
+ items,
+ kind = 'unordered',
+ titleLevel,
+}) => {
+ const kindModifier = `wrapper--${kind}`;
+
+ /**
+ * Format the cards data to be used by the List component.
+ *
+ * @param {CardsListItem[]} cards - An array of card data.
+ * @returns {ListItem[]} The formatted cards data.
+ */
+ const getCards = (cards: CardsListItem[]): ListItem[] => {
+ return cards.map(({ id, ...card }) => {
+ return {
+ id,
+ value: (
+ <Card
+ key={id}
+ coverFit={coverFit}
+ titleLevel={titleLevel}
+ className={styles.card}
+ {...card}
+ />
+ ),
+ };
+ });
+ };
+
+ return (
+ <List
+ items={getCards(items)}
+ withMargin={false}
+ className={`${styles.wrapper} ${styles[kindModifier]}`}
+ />
+ );
+};
+
+export default CardsList;