From c153f93dc8691a71dc76aad3dd618298da9d238a Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 17 Oct 2023 19:46:08 +0200 Subject: refactor(components): rewrite Card component * make the component more generic * merge `` and `` styles into card component to avoid repeating the same structure * remove most of the props to use composition However the CSS is a bit complex because of the two variants... Also, the component should be refactored when the CSS pseudo-class `:has` has enough support: the provider and the `cover` and `meta` props should be removed. --- .../atoms/buttons/button-link/button-link.tsx | 2 +- .../molecules/card/card-actions.test.tsx | 37 ++ src/components/molecules/card/card-actions.tsx | 42 ++ src/components/molecules/card/card-body.tsx | 18 + src/components/molecules/card/card-cover.test.tsx | 23 + src/components/molecules/card/card-cover.tsx | 29 ++ src/components/molecules/card/card-footer.tsx | 32 ++ src/components/molecules/card/card-header.tsx | 32 ++ src/components/molecules/card/card-meta.tsx | 16 + src/components/molecules/card/card-provider.tsx | 43 ++ src/components/molecules/card/card-title.test.tsx | 24 + src/components/molecules/card/card-title.tsx | 27 + src/components/molecules/card/card.module.scss | 296 +++++++++++ src/components/molecules/card/card.stories.tsx | 545 +++++++++++++++++++++ src/components/molecules/card/card.test.tsx | 129 +++++ src/components/molecules/card/card.tsx | 112 +++++ src/components/molecules/card/index.ts | 8 + src/components/molecules/index.ts | 1 + src/components/molecules/layout/card.module.scss | 90 ---- src/components/molecules/layout/card.stories.tsx | 208 -------- src/components/molecules/layout/card.test.tsx | 69 --- src/components/molecules/layout/card.tsx | 88 ---- src/components/molecules/layout/index.ts | 1 - .../organisms/layout/cards-list.module.scss | 2 +- .../organisms/layout/cards-list.stories.tsx | 145 ++---- .../organisms/layout/cards-list.test.tsx | 93 ++-- src/components/organisms/layout/cards-list.tsx | 25 +- .../organisms/layout/comment.module.scss | 150 ++---- src/components/organisms/layout/comment.tsx | 121 +++-- .../organisms/layout/summary.module.scss | 105 +--- src/components/organisms/layout/summary.tsx | 92 ++-- src/i18n/en.json | 8 + src/i18n/fr.json | 8 + src/pages/index.tsx | 59 ++- src/pages/projets/index.tsx | 72 ++- src/styles/abstracts/placeholders/_buttons.scss | 2 +- 36 files changed, 1796 insertions(+), 958 deletions(-) create mode 100644 src/components/molecules/card/card-actions.test.tsx create mode 100644 src/components/molecules/card/card-actions.tsx create mode 100644 src/components/molecules/card/card-body.tsx create mode 100644 src/components/molecules/card/card-cover.test.tsx create mode 100644 src/components/molecules/card/card-cover.tsx create mode 100644 src/components/molecules/card/card-footer.tsx create mode 100644 src/components/molecules/card/card-header.tsx create mode 100644 src/components/molecules/card/card-meta.tsx create mode 100644 src/components/molecules/card/card-provider.tsx create mode 100644 src/components/molecules/card/card-title.test.tsx create mode 100644 src/components/molecules/card/card-title.tsx create mode 100644 src/components/molecules/card/card.module.scss create mode 100644 src/components/molecules/card/card.stories.tsx create mode 100644 src/components/molecules/card/card.test.tsx create mode 100644 src/components/molecules/card/card.tsx create mode 100644 src/components/molecules/card/index.ts delete mode 100644 src/components/molecules/layout/card.module.scss delete mode 100644 src/components/molecules/layout/card.stories.tsx delete mode 100644 src/components/molecules/layout/card.test.tsx delete mode 100644 src/components/molecules/layout/card.tsx (limited to 'src') diff --git a/src/components/atoms/buttons/button-link/button-link.tsx b/src/components/atoms/buttons/button-link/button-link.tsx index f8bbadc..96f5d3e 100644 --- a/src/components/atoms/buttons/button-link/button-link.tsx +++ b/src/components/atoms/buttons/button-link/button-link.tsx @@ -27,7 +27,7 @@ export type ButtonLinkProps = Omit< * * @default 'rectangle' */ - shape?: 'circle' | 'rectangle' | 'square'; + shape?: 'auto' | 'circle' | 'rectangle' | 'square'; /** * Define an URL or anchor as target. */ diff --git a/src/components/molecules/card/card-actions.test.tsx b/src/components/molecules/card/card-actions.test.tsx new file mode 100644 index 0000000..94c2677 --- /dev/null +++ b/src/components/molecules/card/card-actions.test.tsx @@ -0,0 +1,37 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { CardActions } from './card-actions'; + +describe('CardActions', () => { + it('renders its children', () => { + const actions = 'animi et omnis'; + + render({actions}); + + expect(rtlScreen.getByText(actions)).toBeInTheDocument(); + }); + + it('can render its children with start alignment', () => { + const actions = 'animi et omnis'; + + render({actions}); + + expect(rtlScreen.getByText(actions)).toHaveStyle(`--alignment: flex-start`); + }); + + it('can render its children with centered alignment', () => { + const actions = 'animi et omnis'; + + render({actions}); + + expect(rtlScreen.getByText(actions)).toHaveStyle(`--alignment: center`); + }); + + it('can render its children with end alignment', () => { + const actions = 'animi et omnis'; + + render({actions}); + + expect(rtlScreen.getByText(actions)).toHaveStyle(`--alignment: flex-end`); + }); +}); diff --git a/src/components/molecules/card/card-actions.tsx b/src/components/molecules/card/card-actions.tsx new file mode 100644 index 0000000..9d3e09e --- /dev/null +++ b/src/components/molecules/card/card-actions.tsx @@ -0,0 +1,42 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + forwardRef, + type ReactNode, +} from 'react'; +import styles from './card.module.scss'; + +export type CardActionsProps = Omit< + HTMLAttributes, + 'children' +> & { + /** + * The actions alignment. + * + * @default 'end' + */ + alignment?: 'center' | 'end' | 'start'; + /** + * The card actions (ie. buttons, links...). + */ + children: ReactNode; +}; + +const CardActionsWithRef: ForwardRefRenderFunction< + HTMLDivElement, + CardActionsProps +> = ({ alignment = 'end', children, className = '', style, ...props }, ref) => { + const actionsClass = `${styles.actions} ${className}`; + const actionsStyles = { + ...style, + '--alignment': alignment === 'center' ? alignment : `flex-${alignment}`, + }; + + return ( +
+ {children} +
+ ); +}; + +export const CardActions = forwardRef(CardActionsWithRef); diff --git a/src/components/molecules/card/card-body.tsx b/src/components/molecules/card/card-body.tsx new file mode 100644 index 0000000..40cf515 --- /dev/null +++ b/src/components/molecules/card/card-body.tsx @@ -0,0 +1,18 @@ +import type { FC, HTMLAttributes } from 'react'; +import styles from './card.module.scss'; + +export type CardBodyProps = HTMLAttributes; + +export const CardBody: FC = ({ + children, + className = '', + ...props +}) => { + const bodyClass = `${styles.body} ${className}`; + + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/molecules/card/card-cover.test.tsx b/src/components/molecules/card/card-cover.test.tsx new file mode 100644 index 0000000..541da44 --- /dev/null +++ b/src/components/molecules/card/card-cover.test.tsx @@ -0,0 +1,23 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import NextImage from 'next/image'; +import { CardCover } from './card-cover'; + +describe('CardCover', () => { + it('renders a cover', () => { + const altTxt = 'nam cupiditate ex'; + + render( + + + + ); + + expect(rtlScreen.getByRole('img', { name: altTxt })).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/card/card-cover.tsx b/src/components/molecules/card/card-cover.tsx new file mode 100644 index 0000000..3d6bc7b --- /dev/null +++ b/src/components/molecules/card/card-cover.tsx @@ -0,0 +1,29 @@ +import { + type ForwardRefRenderFunction, + type ReactElement, + forwardRef, +} from 'react'; +import { Figure, type FigureProps } from '../../atoms'; +import styles from './card.module.scss'; + +export type CardCoverProps = Omit & { + /** + * The cover. + */ + children: ReactElement; +}; + +const CardCoverWithRef: ForwardRefRenderFunction< + HTMLElement, + CardCoverProps +> = ({ className = '', children, ...props }, ref) => { + const coverClass = `${styles.cover} ${className}`; + + return ( +
+ {children} +
+ ); +}; + +export const CardCover = forwardRef(CardCoverWithRef); diff --git a/src/components/molecules/card/card-footer.tsx b/src/components/molecules/card/card-footer.tsx new file mode 100644 index 0000000..56a6513 --- /dev/null +++ b/src/components/molecules/card/card-footer.tsx @@ -0,0 +1,32 @@ +import { + forwardRef, + type ForwardRefRenderFunction, + type ReactNode, +} from 'react'; +import { Footer, type FooterProps } from '../../atoms'; +import { useCardFooterMeta } from './card-provider'; +import styles from './card.module.scss'; + +export type CardFooterProps = Omit & { + /** + * The card footer contents. + */ + children?: ReactNode; +}; + +const CardFooterWithRef: ForwardRefRenderFunction< + HTMLElement, + CardFooterProps +> = ({ children, className = '', ...props }, ref) => { + const footerClass = `${styles.footer} ${className}`; + const meta = useCardFooterMeta(); + + return ( +
+ {children} + {meta} +
+ ); +}; + +export const CardFooter = forwardRef(CardFooterWithRef); diff --git a/src/components/molecules/card/card-header.tsx b/src/components/molecules/card/card-header.tsx new file mode 100644 index 0000000..213d1bc --- /dev/null +++ b/src/components/molecules/card/card-header.tsx @@ -0,0 +1,32 @@ +import { + forwardRef, + type ForwardRefRenderFunction, + type ReactNode, +} from 'react'; +import { Header, type HeaderProps } from '../../atoms'; +import { useCardCover } from './card-provider'; +import styles from './card.module.scss'; + +export type CardHeaderProps = Omit & { + /** + * The card header contents. + */ + children?: ReactNode; +}; + +const CardHeaderWithRef: ForwardRefRenderFunction< + HTMLElement, + CardHeaderProps +> = ({ children, className = '', ...props }, ref) => { + const cover = useCardCover(); + const headerClass = `${styles.header} ${className}`; + + return ( +
+ {cover} + {children} +
+ ); +}; + +export const CardHeader = forwardRef(CardHeaderWithRef); diff --git a/src/components/molecules/card/card-meta.tsx b/src/components/molecules/card/card-meta.tsx new file mode 100644 index 0000000..403d543 --- /dev/null +++ b/src/components/molecules/card/card-meta.tsx @@ -0,0 +1,16 @@ +import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { MetaList, type MetaListProps } from '../meta-list'; +import styles from './card.module.scss'; + +export type CardMetaProps = MetaListProps; + +const CardMetaWithRef: ForwardRefRenderFunction< + HTMLDListElement, + CardMetaProps +> = ({ className = '', ...props }, ref) => { + const metaClass = `${styles.meta} ${className}`; + + return ; +}; + +export const CardMeta = forwardRef(CardMetaWithRef); diff --git a/src/components/molecules/card/card-provider.tsx b/src/components/molecules/card/card-provider.tsx new file mode 100644 index 0000000..5bcdb49 --- /dev/null +++ b/src/components/molecules/card/card-provider.tsx @@ -0,0 +1,43 @@ +import { + createContext, + type FC, + type ReactElement, + type ReactNode, + useContext, +} from 'react'; + +export type CardCoverProviderProps = { + children: ReactNode; + cover?: ReactElement; +}; + +export const CardCoverContext = createContext(null); + +export const useCardCover = () => useContext(CardCoverContext); + +export const CardCoverProvider: FC = ({ + children, + cover, +}) => ( + + {children} + +); + +export type CardFooterMetaProviderProps = { + children: ReactNode; + meta?: ReactElement; +}; + +export const CardFooterMetaContext = createContext(null); + +export const useCardFooterMeta = () => useContext(CardFooterMetaContext); + +export const CardFooterMetaProvider: FC = ({ + children, + meta, +}) => ( + + {children} + +); diff --git a/src/components/molecules/card/card-title.test.tsx b/src/components/molecules/card/card-title.test.tsx new file mode 100644 index 0000000..9dbf6ac --- /dev/null +++ b/src/components/molecules/card/card-title.test.tsx @@ -0,0 +1,24 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { CardTitle } from './card-title'; + +describe('CardTitle', () => { + it('renders a title of level 2', () => { + const title = 'animi et omnis'; + + render({title}); + + expect(rtlScreen.getByRole('heading', { level: 2 })).toHaveTextContent( + title + ); + }); + + it('can render a title with a custom level', () => { + const level = 4; + const title = 'animi et omnis'; + + render({title}); + + expect(rtlScreen.getByRole('heading', { level })).toHaveTextContent(title); + }); +}); diff --git a/src/components/molecules/card/card-title.tsx b/src/components/molecules/card/card-title.tsx new file mode 100644 index 0000000..09d61ea --- /dev/null +++ b/src/components/molecules/card/card-title.tsx @@ -0,0 +1,27 @@ +import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { Heading, type HeadingProps } from '../../atoms'; +import styles from './card.module.scss'; + +export type CardTitleProps = Omit & { + /** + * The title level (between 1 and 6). + * + * @default 2 + */ + level?: HeadingProps['level']; +}; + +const CardTitleWithRef: ForwardRefRenderFunction< + HTMLHeadingElement, + CardTitleProps +> = ({ children, className = '', level = 2, ...props }, ref) => { + const headingClass = `${styles.title} ${className}`; + + return ( + + {children} + + ); +}; + +export const CardTitle = forwardRef(CardTitleWithRef); diff --git a/src/components/molecules/card/card.module.scss b/src/components/molecules/card/card.module.scss new file mode 100644 index 0000000..65f92f6 --- /dev/null +++ b/src/components/molecules/card/card.module.scss @@ -0,0 +1,296 @@ +@use "../../../styles/abstracts/functions" as fun; + +$breakpoint: 50ch; + +.cover { + width: var(--cover-width, 100%); + height: var(--cover-height, fun.convert-px(150)); + max-width: none; + position: relative; + + > * { + width: 100%; + height: 100%; + max-width: 100%; + position: absolute; + inset: 0; + object-position: center; + object-fit: cover; + } +} + +.title { + background: none; + padding: 0; +} + +.body { + max-width: 80ch; + color: var(--color-fg); + font-weight: 400; +} + +.actions { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: var(--alignment); + column-gap: var(--spacing-md); + row-gap: var(--spacing-sm); + padding-block: var(--spacing-sm); +} + +.meta { + font-size: var(--font-size-sm); +} + +:where(.footer) .meta { + flex-flow: row wrap; +} + +.header, +.footer { + display: contents; +} + +.card { + --card-padding: clamp(var(--spacing-sm), 3vw, var(--spacing-md)); + + column-gap: var(--spacing-md); + row-gap: var(--spacing-sm); + width: 100%; + height: 100%; + background: var(--color-bg); + + &--variant-1 { + --cover-height: clamp( + #{fun.convert-px(100)}, + calc(#{fun.convert-px(200)} - 10cqw), + #{fun.convert-px(200)} + ); + } + + &--variant-2 { + --cover-height: #{fun.convert-px(90)}; + --cover-width: var(--cover-height); + } +} + +:where(.card--variant-2) .cover { + border-radius: fun.convert-px(3); + box-shadow: + 0 0 0 fun.convert-px(1) var(--color-shadow-light), + fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(1) var(--color-shadow); +} + +.wrapper { + container: card / inline-size; + + &--centered { + .header, + .body, + .footer { + text-align: center; + } + + .meta { + margin-inline: auto; + } + } + + &--is-link { + --scale-up: 1.05; + + &:not(:disabled):focus:not(:hover) { + text-decoration: none; + + .title { + text-decoration: underline solid var(--color-primary) 0.3ex; + } + } + } +} + +/* stylelint-disable no-descending-specificity -- Stylelint complains about + * specificity but I think it is clearer and DRY this way. */ + +:where(.wrapper--is-block) .card--variant-2 { + border: fun.convert-px(1) solid var(--color-border); + box-shadow: + fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light), + fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2) + var(--color-shadow); +} + +@container card (width <= #{$breakpoint}) { + .card { + display: flex; + flex-flow: column wrap; + row-gap: var(--card-padding); + + &:where(:not(&--has-cover)) { + padding: var(--card-padding); + } + + &:where(&--has-cover) { + padding-block-end: var(--card-padding); + + :where(.header, .footer) > *:not(.cover), + .body { + padding-inline: var(--card-padding); + } + } + } + + :where(.card--variant-1) { + .cover { + --cover-width: 100%; + } + + :where(.footer) .meta { + margin-block-start: auto; + justify-content: space-between; + } + } + + :where(.card--variant-2) { + :where(.header) { + .cover, + .title { + margin-inline: auto; + } + + .cover { + margin-block-start: var(--card-padding); + } + } + } +} + +@container card (width > #{$breakpoint}) { + .card { + display: grid; + grid-auto-rows: max-content; + padding: var(--card-padding); + } + + .cover, + .title, + .meta, + .body, + .actions { + grid-column: 1; + } + + .card--variant-1 { + grid-auto-columns: minmax(30ch, 80ch) 1fr minmax(min-content, 25cqw); + } + + :where(.wrapper--is-block) .card--variant-1 { + border: fun.convert-px(1) solid var(--color-primary-dark); + border-radius: fun.convert-px(3); + box-shadow: + fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 + var(--color-shadow), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) fun.convert-px(-1) + var(--color-shadow-light), + fun.convert-px(5) fun.convert-px(5) fun.convert-px(7) fun.convert-px(-1) + var(--color-shadow-light); + } + + .card--variant-2 { + grid-auto-columns: minmax(min-content, 15cqw) 1fr minmax(30ch, 80ch); + } + + .cover { + grid-row-start: 1; + } + + :where(.wrapper--is-link:not(:disabled, :hover):focus) .title { + text-decoration: underline solid var(--color-primary) 0.3ex; + } + + :where(.card--has-cover .header:only-child) .cover:only-child { + --cover-width: calc(100% + (2 * var(--card-padding))); + --cover-height: calc(100% + (2 * var(--card-padding))); + + margin-block-start: calc(var(--card-padding) * -1); + margin-inline-start: calc(var(--card-padding) * -1); + } + + :where(.card--variant-1) { + :where(.header:only-child) > *:only-child, + .body:only-child, + :where(.footer:only-child) > *:only-child { + grid-column: 1 / span 2; + } + + :where(.header:not(:only-child)) .cover, + :where(.header:only-child) .cover:not(:only-child), + :where(.footer:not(:only-child)) .meta, + :where(.footer:only-child) .meta:not(:only-child) { + grid-column: 3; + } + + :where(.footer) .meta { + grid-row-start: 1; + flex-flow: column wrap; + } + + :where(.body:first-child + .footer) .meta, + :where(.footer:only-child) .meta { + grid-row-end: 1; + } + + :where(.header) .title, + :where(.header) .cover + .meta { + grid-row: 1; + align-self: center; + } + } + + :where(.card--variant-1.card--has-cover .footer) .meta { + grid-row: 2 / span 3; + } + + :where(.card--variant-1.card--no-footer-meta .header:not(:only-child)) .cover, + :where(.card--variant-1.card--no-footer-meta .header) .cover:not(:only-child), + :where(.card--variant-1.card--no-cover .footer) .meta { + --cover-height: 100%; + + grid-row-end: span 4; + } + + :where(.card--variant-2) { + .header:only-child > *, + .body:only-child, + .footer:only-child > * { + grid-column: 1 / span 2; + } + + .header:only-child > .meta { + justify-self: center; + } + + :where(.header:not(:only-child)) .meta, + .body, + :where(.footer:not(:only-child)) .actions, + :where(.footer:not(:only-child)) .meta { + grid-column: 3; + } + + :where(.header) { + .cover, + .title { + grid-row: 1 / span 2; + justify-self: center; + } + } + } + + :where(.card--variant-2.card--has-cover .header) .title { + margin-top: calc(var(--cover-height) + var(--spacing-sm)); + } +} + +/* stylelint-enable no-descending-specificity */ diff --git a/src/components/molecules/card/card.stories.tsx b/src/components/molecules/card/card.stories.tsx new file mode 100644 index 0000000..74f426c --- /dev/null +++ b/src/components/molecules/card/card.stories.tsx @@ -0,0 +1,545 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import NextImage from 'next/image'; +import { Button, ButtonLink, Link, Time } from '../../atoms'; +import { Card, type CardProps } from './card'; +import { CardActions } from './card-actions'; +import { CardBody } from './card-body'; +import { CardCover } from './card-cover'; +import { CardFooter } from './card-footer'; +import { CardHeader } from './card-header'; +import { CardMeta } from './card-meta'; +import { CardTitle } from './card-title'; + +/** + * Card - Storybook Meta + */ +export default { + title: 'Molecules/Card', + component: Card, + argTypes: {}, +} as ComponentMeta; + +const Template: ComponentStory = ( + args: CardProps +) => ; + +/** + * Card Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + children: 'The card contents.', +}; + +/** + * Card Stories - AsLink + */ +export const AsLink = Template.bind({}); +AsLink.args = { + 'aria-label': 'Learn more about this card', + children: 'The card contents.', + linkTo: '#card', +}; + +export const HeaderCover = Template.bind({}); +HeaderCover.args = { + children: , + cover: ( + + + + ), +}; + +export const HeaderTitle = Template.bind({}); +HeaderTitle.args = { + children: ( + + The card title + + ), +}; + +export const HeaderMeta = Template.bind({}); +HeaderMeta.args = { + children: ( + + , + }, + ]} + /> + + ), +}; + +export const BodyContents = Template.bind({}); +BodyContents.args = { + children: The card contents, +}; + +export const FooterActions = Template.bind({}); +FooterActions.args = { + children: ( + + + Read more + + + + ), +}; + +export const FooterMeta = Template.bind({}); +FooterMeta.args = { + children: , + meta: ( + Category 1 }, + { id: 'cat-2', value: Category 2 }, + ], + }, + { + id: 'tags', + label: 'Tags:', + value: [ + { id: 'tag-1', value: 'Tag 1' }, + { id: 'tag-2', value: 'Tag 2' }, + { id: 'tag-3', value: 'Tag 3' }, + ], + }, + ]} + /> + ), +}; + +export const CompositionCoverTitle = Template.bind({}); +CompositionCoverTitle.args = { + children: ( + + The card title + + ), + cover: ( + + + + ), +}; + +export const CompositionTitleMeta = Template.bind({}); +CompositionTitleMeta.args = { + children: ( + + The card title + , + }, + ]} + /> + + ), +}; + +export const CompositionCoverTitleMeta = Template.bind({}); +CompositionCoverTitleMeta.args = { + children: ( + + The card title + , + }, + ]} + /> + + ), + cover: ( + + + + ), +}; + +export const CompositionTitleBody = Template.bind({}); +CompositionTitleBody.args = { + children: ( + <> + + The card title + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + ), +}; + +export const CompositionCoverTitleBody = Template.bind({}); +CompositionCoverTitleBody.args = { + children: ( + <> + + The card title + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + ), + cover: ( + + + + ), +}; + +export const CompositionTitleMetaBody = Template.bind({}); +CompositionTitleMetaBody.args = { + children: ( + <> + + The card title + , + }, + ]} + /> + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + ), +}; + +export const CompositionCoverTitleMetaBody = Template.bind({}); +CompositionCoverTitleMetaBody.args = { + children: ( + <> + + The card title + , + }, + ]} + /> + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + ), + cover: ( + + + + ), +}; + +export const CompositionTitleActions = Template.bind({}); +CompositionTitleActions.args = { + children: ( + <> + + The card title + + + + Read more + + + + + ), +}; + +export const CompositionCoverTitleActions = Template.bind({}); +CompositionCoverTitleActions.args = { + children: ( + <> + + The card title + + + + Read more + + + + + ), + cover: ( + + + + ), +}; + +export const CompositionTitleBodyActions = Template.bind({}); +CompositionTitleBodyActions.args = { + children: ( + <> + + The card title + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + + Read more + + + + + ), +}; + +export const CompositionTitleBodyActionsMeta = Template.bind({}); +CompositionTitleBodyActionsMeta.args = { + children: ( + <> + + The card title + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + + Read more + + + + + ), + meta: ( + Category 1 }, + { id: 'cat-2', value: Category 2 }, + ], + }, + { + id: 'tags', + label: 'Tags:', + value: [ + { id: 'tag-1', value: 'Tag 1' }, + { id: 'tag-2', value: 'Tag 2' }, + { id: 'tag-3', value: 'Tag 3' }, + ], + }, + ]} + /> + ), +}; + +export const CompositionCoverTitleBodyActionsMeta = Template.bind({}); +CompositionCoverTitleBodyActionsMeta.args = { + children: ( + <> + + The card title + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + + Read more + + + + + ), + cover: ( + + + + ), + meta: ( + Category 1 }, + { id: 'cat-2', value: Category 2 }, + ], + }, + { + id: 'tags', + label: 'Tags:', + value: [ + { id: 'tag-1', value: 'Tag 1' }, + { id: 'tag-2', value: 'Tag 2' }, + { id: 'tag-3', value: 'Tag 3' }, + ], + }, + ]} + /> + ), +}; + +export const CompositionAllContents = Template.bind({}); +CompositionAllContents.args = { + children: ( + <> + + The card title + , + }, + ]} + /> + + + Nihil magnam tempora voluptatem. Reiciendis ut cum vel. Odit et + necessitatibus esse laudantium sequi ad. Et est quas pariatur facere + velit veritatis nihil. Ratione aperiam omnis quia ut asperiores tenetur + dolores veniam. Nostrum est ullam aliquam aliquid expedita ea. + + + + Read more + + + + + ), + cover: ( + + + + ), + meta: ( + Category 1 }, + { id: 'cat-2', value: Category 2 }, + ], + }, + { + id: 'tags', + label: 'Tags:', + value: [ + { id: 'tag-1', value: 'Tag 1' }, + { id: 'tag-2', value: 'Tag 2' }, + { id: 'tag-3', value: 'Tag 3' }, + ], + }, + ]} + /> + ), +}; diff --git a/src/components/molecules/card/card.test.tsx b/src/components/molecules/card/card.test.tsx new file mode 100644 index 0000000..40a5830 --- /dev/null +++ b/src/components/molecules/card/card.test.tsx @@ -0,0 +1,129 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import NextImage from 'next/image'; +import { Card } from './card'; +import { CardFooter } from './card-footer'; +import { CardHeader } from './card-header'; +import { CardMeta } from './card-meta'; + +describe('Card', () => { + it('renders its children', () => { + const body = 'eveniet error voluptas'; + + render({body}); + + expect(rtlScreen.getByText(body)).toBeInTheDocument(); + }); + + it('can render a cover in the card header', () => { + const altTxt = 'quo expedita eveniet'; + + render( + + } + > + + + ); + + expect(rtlScreen.getByRole('img', { name: altTxt })).toBeInTheDocument(); + }); + + it('does not render a cover without card header', () => { + const body = 'necessitatibus maiores sed'; + const altTxt = 'quo expedita eveniet'; + + render( + + } + > + {body} + + ); + + expect( + rtlScreen.queryByRole('img', { name: altTxt }) + ).not.toBeInTheDocument(); + }); + + it('can render some meta in the card footer', () => { + const term = 'ut'; + const desc = 'repudiandae'; + + render( + } + > + + + ); + + expect(rtlScreen.getByRole('term')).toHaveTextContent(term); + expect(rtlScreen.getByRole('definition')).toHaveTextContent(desc); + }); + + it('does not render the meta without card footer', () => { + const body = 'rerum dolore et'; + const term = 'ut'; + const desc = 'repudiandae'; + + render( + } + > + {body} + + ); + + expect( + rtlScreen.queryByRole('term', { name: term }) + ).not.toBeInTheDocument(); + expect( + rtlScreen.queryByRole('definition', { name: desc }) + ).not.toBeInTheDocument(); + }); + + it('can render a card as link to another page', () => { + const body = 'Et qui harum voluptas est quos qui.'; + const cta = 'asperiores optio incidunt'; + const target = '#molestiae'; + + render( + + {body} + + ); + + expect(rtlScreen.getByRole('link', { name: cta })).toHaveAttribute( + 'href', + target + ); + }); + + it('can render a card with centered text', () => { + const body = 'Et qui harum voluptas est quos qui.'; + const label = 'asperiores optio incidunt'; + + render( + + {body} + + ); + + expect(rtlScreen.getByLabelText(label)).toHaveClass('wrapper--centered'); + }); +}); diff --git a/src/components/molecules/card/card.tsx b/src/components/molecules/card/card.tsx new file mode 100644 index 0000000..788b040 --- /dev/null +++ b/src/components/molecules/card/card.tsx @@ -0,0 +1,112 @@ +import { + forwardRef, + type HTMLAttributes, + type ForwardedRef, + type ReactNode, + type ReactElement, +} from 'react'; +import { Article, ButtonLink, type ButtonLinkProps } from '../../atoms'; +import { CardCoverProvider, CardFooterMetaProvider } from './card-provider'; +import styles from './card.module.scss'; + +type CardBaseProps = T extends string + ? Omit + : Omit, 'children'>; + +export type CardProps = CardBaseProps & { + /** + * The card contents. + */ + children: ReactNode; + /** + * The card cover. You need to add a `` as children to use it. + */ + cover?: ReactElement; + /** + * Should the contents be centered? + * + * @default false + */ + isCentered?: boolean; + /** + * Link the card to another page. + * + * @default undefined + */ + linkTo?: T; + /** + * The meta to place in the card footer. You need to add a `` + * as children to use it. + */ + meta?: ReactElement; + /** + * The card variant. + * + * @default 1 + */ + variant?: 1 | 2; +}; + +const CardWrapper = ( + { + children, + className = '', + cover, + isCentered = false, + linkTo, + meta, + variant = 1, + ...props + }: CardProps, + ref: ForwardedRef +) => { + const wrapperClass = [ + styles.wrapper, + styles[isCentered ? 'wrapper--centered' : 'wrapper--not-centered'], + styles[linkTo ? 'wrapper--is-link' : 'wrapper--is-block'], + className, + ].join(' '); + const cardClass = [ + styles.card, + styles[cover ? 'card--has-cover' : 'card--no-cover'], + styles[meta ? 'card--has-footer-meta' : 'card--no-footer-meta'], + styles[`card--variant-${variant}`], + ].join(' '); + + return ( + + + {linkTo ? ( + )} + className={wrapperClass} + ref={ref} + // eslint-disable-next-line react/jsx-no-literals -- Shape allowed + shape="auto" + to={linkTo} + > +
{children}
+
+ ) : ( +
)} + className={wrapperClass} + ref={ref as ForwardedRef} + > +
{children}
+
+ )} +
+
+ ); +}; + +/** + * Card component + * + * Render a card with title and some other optional data. + * + * TODO: remove `cover` and `meta` props (and adapt CSS) when support for CSS + * `:has()` pseudo-class will be good enough. + */ +export const Card = forwardRef(CardWrapper); diff --git a/src/components/molecules/card/index.ts b/src/components/molecules/card/index.ts new file mode 100644 index 0000000..2b55191 --- /dev/null +++ b/src/components/molecules/card/index.ts @@ -0,0 +1,8 @@ +export * from './card'; +export * from './card-actions'; +export * from './card-body'; +export * from './card-cover'; +export * from './card-footer'; +export * from './card-header'; +export * from './card-meta'; +export * from './card-title'; diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index a1e2c7a..b042a96 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -1,5 +1,6 @@ export * from './branding'; export * from './buttons'; +export * from './card'; export * from './code'; export * from './collapsible'; export * from './forms'; diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss deleted file mode 100644 index 14a5baf..0000000 --- a/src/components/molecules/layout/card.module.scss +++ /dev/null @@ -1,90 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; - -.footer { - margin-top: auto; -} - -.wrapper { - --scale-up: 1.05; - --scale-down: 0.95; - - display: flex; - flex-flow: column wrap; - max-width: var(--card-width, 40ch); - padding: 0; - text-align: center; - - .article { - flex: 1; - display: flex; - flex-flow: column nowrap; - justify-content: flex-start; - } - - .cover { - place-content: center; - height: fun.convert-px(150); - object-fit: scale-down; - margin: auto; - border-bottom: fun.convert-px(1) solid var(--color-border); - } - - .title, - .tagline, - .footer { - padding: 0 var(--spacing-md); - } - - .title { - width: fit-content; - margin: var(--spacing-sm) auto; - } - - h2.title { - background: none; - text-shadow: none; - } - - .tagline { - flex: 1; - margin-bottom: var(--spacing-md); - color: var(--color-fg); - font-weight: 400; - } - - .list { - margin-bottom: var(--spacing-md); - } - - .meta { - &__item { - flex-flow: row wrap; - place-content: center; - gap: var(--spacing-2xs); - margin: auto; - } - - &__label { - flex: 0 0 100%; - } - - &__value { - padding: fun.convert-px(2) var(--spacing-xs); - border: fun.convert-px(1) solid var(--color-primary-darker); - color: var(--color-fg); - font-weight: 400; - - &::before { - display: none; - } - } - } - - &:not(:disabled):focus { - text-decoration: none; - - .title { - text-decoration: underline solid var(--color-primary) 0.3ex; - } - } -} diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx deleted file mode 100644 index 070c978..0000000 --- a/src/components/molecules/layout/card.stories.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import type { MetaItemData } from '../meta-list'; -import { Card } from './card'; - -/** - * Card - Storybook Meta - */ -export default { - title: 'Molecules/Layout/Card', - component: Card, - argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the card wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - cover: { - description: 'The card cover data (src, dimensions, alternative text).', - table: { - category: 'Options', - }, - type: { - name: 'object', - required: false, - value: {}, - }, - }, - coverFit: { - control: { - type: 'select', - }, - description: 'The cover fit.', - options: ['contain', 'cover', 'fill', 'scale-down'], - table: { - category: 'Options', - defaultValue: { summary: 'cover' }, - }, - type: { - name: 'string', - required: false, - }, - }, - id: { - control: { - type: 'text', - }, - description: 'The card id.', - type: { - name: 'string', - required: true, - }, - }, - meta: { - description: 'The card metadata (a publication date for example).', - table: { - category: 'Options', - }, - type: { - name: 'object', - required: false, - value: {}, - }, - }, - tagline: { - control: { - type: 'text', - }, - description: 'A few words about the card.', - table: { - category: 'Options', - }, - type: { - name: 'string', - required: false, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The card title.', - type: { - name: 'string', - required: true, - }, - }, - titleLevel: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The title level.', - type: { - name: 'number', - required: true, - }, - }, - url: { - control: { - type: 'text', - }, - description: 'The card target.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ; - -const cover = { - alt: 'A picture', - height: 480, - src: 'https://picsum.photos/640/480', - width: 640, -}; - -const id = 'nam'; - -const meta = [ - { id: 'author', label: 'Author', value: 'Possimus' }, - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'autem', value: 'Autem' }, - { id: 'eos', value: 'Eos' }, - ], - }, -] satisfies MetaItemData[]; - -const tagline = 'Ut rerum incidunt'; - -const title = 'Alias qui porro'; - -const url = '/an-existing-url'; - -/** - * Card Stories - Default - */ -export const Default = Template.bind({}); -Default.args = { - id, - title, - titleLevel: 2, - url, -}; - -/** - * Card Stories - With cover - */ -export const WithCover = Template.bind({}); -WithCover.args = { - cover, - id, - title, - titleLevel: 2, - url, -}; - -/** - * Card Stories - With meta - */ -export const WithMeta = Template.bind({}); -WithMeta.args = { - id, - meta, - title, - titleLevel: 2, - url, -}; - -/** - * Card Stories - With tagline - */ -export const WithTagline = Template.bind({}); -WithTagline.args = { - id, - tagline, - title, - titleLevel: 2, - url, -}; - -/** - * Card Stories - With all data - */ -export const WithAll = Template.bind({}); -WithAll.args = { - cover, - id, - meta, - tagline, - title, - titleLevel: 2, - url, -}; diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx deleted file mode 100644 index b690d4c..0000000 --- a/src/components/molecules/layout/card.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import type { MetaItemData } from '../meta-list'; -import { Card } from './card'; - -const cover = { - alt: 'A picture', - height: 480, - src: 'https://picsum.photos/640/480', - width: 640, -}; - -const id = 'nam'; - -const meta = [ - { id: 'author', label: 'Author', value: 'Possimus' }, - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'autem', value: 'Autem' }, - { id: 'eos', value: 'Eos' }, - ], - }, -] satisfies MetaItemData[]; - -const tagline = 'Ut rerum incidunt'; - -const title = 'Alias qui porro'; - -const url = '/an-existing-url'; - -describe('Card', () => { - it('renders a title wrapped in h2 element', () => { - render(); - expect( - rtlScreen.getByRole('heading', { level: 2, name: title }) - ).toBeInTheDocument(); - }); - - it('renders a link to another page', () => { - render(); - expect(rtlScreen.getByRole('link')).toHaveAttribute('href', url); - }); - - it('renders a cover', () => { - render( - - ); - expect(rtlScreen.getByRole('img', { name: cover.alt })).toBeInTheDocument(); - }); - - it('renders a tagline', () => { - render( - - ); - expect(rtlScreen.getByText(tagline)).toBeInTheDocument(); - }); - - it('renders some meta', () => { - render(); - - const metaLabels = meta.map((item) => item.label); - - for (const label of metaLabels) { - expect(rtlScreen.getByText(label)).toBeInTheDocument(); - } - }); -}); diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx deleted file mode 100644 index d90cba2..0000000 --- a/src/components/molecules/layout/card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import NextImage, { type ImageProps as NextImageProps } from 'next/image'; -import type { FC } from 'react'; -import { ButtonLink, Figure, Heading, type HeadingLevel } from '../../atoms'; -import { MetaList, type MetaItemData } from '../meta-list'; -import styles from './card.module.scss'; - -export type CardProps = { - /** - * Set additional classnames to the card wrapper. - */ - className?: string; - /** - * The card cover. - */ - cover?: Pick; - /** - * The card id. - */ - id: string; - /** - * The card meta. - */ - meta?: MetaItemData[]; - /** - * The card tagline. - */ - tagline?: string; - /** - * The card title. - */ - title: string; - /** - * The title level (hn). - */ - titleLevel: HeadingLevel; - /** - * The card target. - */ - url: string; -}; - -/** - * Card component - * - * Render a link with minimal information about its content. - */ -export const Card: FC = ({ - className = '', - cover, - id, - meta, - tagline, - title, - titleLevel, - url, -}) => { - const cardClass = `${styles.wrapper} ${className}`; - const headingId = `${id}-heading`; - - return ( - -
-
- {cover ? ( -
- -
- ) : null} - - {title} - -
- {tagline ?
{tagline}
: null} - {meta ? ( -
- -
- ) : null} -
-
- ); -}; diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts index 80db10a..75cbe28 100644 --- a/src/components/molecules/layout/index.ts +++ b/src/components/molecules/layout/index.ts @@ -1,4 +1,3 @@ -export * from './card'; export * from './columns'; export * from './page-footer'; export * from './page-header'; diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss index 8b18c08..1665829 100644 --- a/src/components/organisms/layout/cards-list.module.scss +++ b/src/components/organisms/layout/cards-list.module.scss @@ -19,6 +19,6 @@ } } -.card { +.item > * { height: 100%; } diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx index 03feee7..3f8e72a 100644 --- a/src/components/organisms/layout/cards-list.stories.tsx +++ b/src/components/organisms/layout/cards-list.stories.tsx @@ -1,4 +1,5 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Card, CardBody, CardHeader, CardTitle } from '../../molecules'; import { CardsList as CardsListComponent, type CardsListItem, @@ -10,39 +11,7 @@ import { export default { title: 'Organisms/Layout', component: CardsListComponent, - args: { - coverFit: 'cover', - kind: 'unordered', - }, argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the list wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - coverFit: { - control: { - type: 'select', - }, - description: 'The cover fit.', - options: ['fill', 'contain', 'cover', 'none', 'scale-down'], - table: { - category: 'Options', - defaultValue: { summary: 'cover' }, - }, - type: { - name: 'string', - required: false, - }, - }, items: { description: 'The cards data.', type: { @@ -51,33 +20,20 @@ export default { value: {}, }, }, - kind: { + isOrdered: { control: { - type: 'select', + type: 'boolean', }, - description: 'The list kind.', - options: ['ordered', 'unordered'], + description: 'Should the list be ordered?', table: { category: 'Options', - defaultValue: { summary: 'unordered' }, + defaultValue: { summary: false }, }, type: { - name: 'string', + name: 'boolean', required: false, }, }, - titleLevel: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The heading level for each card.', - type: { - name: 'number', - required: true, - }, - }, }, } as ComponentMeta; @@ -88,63 +44,49 @@ const Template: ComponentStory = (args) => ( const items: CardsListItem[] = [ { id: 'card-1', - cover: { - alt: 'card 1 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [ - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'velit', value: 'Velit' }, - { id: 'ex', value: 'Ex' }, - { id: 'alias', value: 'Alias' }, - ], - }, - ], - tagline: 'Molestias ut error', - title: 'Et alias omnis', - url: '#', + card: ( + + + Et alias omnis + + + Rerum voluptatem sint sint sit dignissimos. Labore totam possimus + tempore atque veniam. Doloremque tenetur quidem beatae veritatis quo. + Quaerat voluptatem deleniti voluptas quia. Qui voluptatem iure iste + expedita et sed beatae. + + + ), }, { id: 'card-2', - cover: { - alt: 'card 2 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }], - tagline: 'Quod vel accusamus', - title: 'Laboriosam doloremque mollitia', - url: '#', + card: ( + + + Fugiat magnam nesciunt + + + Sit corporis animi ea. Earum asperiores error et. Aliquid quia et + consequatur. Magnam sit ut facere explicabo vel dolorem earum + assumenda. Aspernatur inventore quod libero est. + + + ), }, { id: 'card-3', - cover: { - alt: 'card 3 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [ - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'quisquam', value: 'Quisquam' }, - { id: 'quia', value: 'Quia' }, - { id: 'sapiente', value: 'Sapiente' }, - { id: 'perspiciatis', value: 'Perspiciatis' }, - ], - }, - ], - tagline: 'Quo error eum', - title: 'Magni rem nulla', - url: '#', + card: ( + + + Asperiores eum quas + + + Doloremque ut cupiditate distinctio aperiam. Neque tempora unde + perferendis asperiores. Doloremque velit vel quam. Temporibus itaque + non non exercitationem. + + + ), }, ]; @@ -154,5 +96,4 @@ const items: CardsListItem[] = [ 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 index c9d6ae7..b04e109 100644 --- a/src/components/organisms/layout/cards-list.test.tsx +++ b/src/components/organisms/layout/cards-list.test.tsx @@ -1,73 +1,60 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Card, CardBody, CardHeader, CardTitle } from '../../molecules'; import { CardsList, type CardsListItem } from './cards-list'; const items: CardsListItem[] = [ { id: 'card-1', - cover: { - alt: 'card 1 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [ - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'velit', value: 'Velit' }, - { id: 'ex', value: 'Ex' }, - { id: 'alias', value: 'Alias' }, - ], - }, - ], - tagline: 'Molestias ut error', - title: 'Et alias omnis', - url: '#', + card: ( + + + Et alias omnis + + + Rerum voluptatem sint sint sit dignissimos. Labore totam possimus + tempore atque veniam. Doloremque tenetur quidem beatae veritatis quo. + Quaerat voluptatem deleniti voluptas quia. Qui voluptatem iure iste + expedita et sed beatae. + + + ), }, { id: 'card-2', - cover: { - alt: 'card 2 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }], - tagline: 'Quod vel accusamus', - title: 'Laboriosam doloremque mollitia', - url: '#', + card: ( + + + Fugiat magnam nesciunt + + + Sit corporis animi ea. Earum asperiores error et. Aliquid quia et + consequatur. Magnam sit ut facere explicabo vel dolorem earum + assumenda. Aspernatur inventore quod libero est. + + + ), }, { id: 'card-3', - cover: { - alt: 'card 3 picture', - src: 'https://picsum.photos/640/480', - width: 640, - height: 480, - }, - meta: [ - { - id: 'categories', - label: 'Categories', - value: [ - { id: 'quisquam', value: 'Quisquam' }, - { id: 'quia', value: 'Quia' }, - { id: 'sapiente', value: 'Sapiente' }, - { id: 'perspiciatis', value: 'Perspiciatis' }, - ], - }, - ], - tagline: 'Quo error eum', - title: 'Magni rem nulla', - url: '#', + card: ( + + + Asperiores eum quas + + + Doloremque ut cupiditate distinctio aperiam. Neque tempora unde + perferendis asperiores. Doloremque velit vel quam. Temporibus itaque + non non exercitationem. + + + ), }, ]; describe('CardsList', () => { it('renders a list of cards', () => { - render(); + render(); expect(rtlScreen.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 index 29add3b..4f920e8 100644 --- a/src/components/organisms/layout/cards-list.tsx +++ b/src/components/organisms/layout/cards-list.tsx @@ -1,16 +1,20 @@ -import type { FC } from 'react'; +import type { FC, ReactElement } from 'react'; import { List, ListItem } from '../../atoms'; -import { Card, type CardProps } from '../../molecules'; +import type { CardProps } from '../../molecules'; import styles from './cards-list.module.scss'; -export type CardsListItem = Omit & { +export type CardsListItem = { + /** + * The card. + */ + card: ReactElement | CardProps>; /** * The card id. */ id: string; }; -export type CardsListProps = Pick & { +export type CardsListProps = { /** * Set additional classnames to the list wrapper. */ @@ -36,7 +40,6 @@ export const CardsList: FC = ({ className = '', isOrdered = false, items, - titleLevel, }) => { const kindModifier = `wrapper--${isOrdered ? 'ordered' : 'unordered'}`; @@ -47,15 +50,9 @@ export const CardsList: FC = ({ isInline isOrdered={isOrdered} > - {items.map(({ id, ...item }) => ( - - + {items.map(({ id, card }) => ( + + {card} ))} diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss index bf8aada..f26b3fe 100644 --- a/src/components/organisms/layout/comment.module.scss +++ b/src/components/organisms/layout/comment.module.scss @@ -2,132 +2,72 @@ @use "../../../styles/abstracts/mixins" as mix; @use "../../../styles/abstracts/placeholders"; -.wrapper { - padding: var(--spacing-md); - background: var(--color-bg); - border: fun.convert-px(1) solid var(--color-border); - box-shadow: - fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light), - fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2) - var(--color-shadow); - - &--comment { - @include mix.media("screen") { - @include mix.dimensions("sm") { - display: grid; - grid-template-columns: minmax(0, #{fun.convert-px(150)}) minmax(0, 1fr); - column-gap: var(--spacing-lg); - } - } - } - - &--form { - display: flex; - flex-flow: column wrap; - place-content: center; - margin-top: var(--spacing-sm); - } - - .header { - display: flex; - flex-flow: column wrap; - align-items: center; - row-gap: var(--spacing-sm); - - @include mix.media("screen") { - @include mix.dimensions("sm") { - grid-row: 1 / 4; - } - } - } - - .author { - color: var(--color-primary-darker); - font-weight: 600; - text-align: center; - } - - .avatar { - width: fun.convert-px(85); - height: fun.convert-px(85); - position: relative; +.avatar { + img { border-radius: fun.convert-px(3); box-shadow: 0 0 0 fun.convert-px(1) var(--color-shadow-light), fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(1) var(--color-shadow); - - img { - border-radius: fun.convert-px(3); - } } +} - .date { - margin: var(--spacing-sm) 0; - font-size: var(--font-size-sm); +.author { + color: var(--color-primary-darker); + font-family: var(--font-family-regular); + font-size: var(--font-size-md); + font-weight: 600; + text-align: center; + text-shadow: none; +} - &__item { - justify-content: center; - } +.body { + overflow-wrap: break-word; - @include mix.media("screen") { - @include mix.dimensions("sm") { - margin: 0 0 var(--spacing-sm); + :global { + a { + @extend %link; - &__item { - justify-content: left; - } + &[hreflang], + &.download, + &.external { + @extend %link-with-icon; } - } - } - - .body { - overflow-wrap: break-word; - - :global { - a { - @extend %link; - &[hreflang], - &.download, - &.external { - @extend %link-with-icon; - } - - &[hreflang] { - @extend %link-with-lang; - } + &[hreflang] { + @extend %link-with-lang; + } - &[hreflang]:not(.download, .external) { - --is-icon-hidden: ""; - } + &[hreflang]:not(.download, .external) { + --is-icon-hidden: ""; + } - &.download { - @extend %download-link; - } + &.download { + @extend %download-link; + } - &.external { - @extend %external-link; - } + &.external { + @extend %external-link; + } - &.download, - &.external { - &:not([hreflang]) { - --is-lang-hidden: ""; - } + &.download, + &.external { + &:not([hreflang]) { + --is-lang-hidden: ""; } + } - &.external.download { - @extend %external-download-link; - } + &.external.download { + @extend %external-download-link; } } } +} + +.form { + margin-top: var(--spacing-sm); - .footer { - display: flex; - justify-content: flex-end; - align-items: center; - padding: var(--spacing-md) 0 0; + form > * { + margin-inline: auto; } } diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index cb2f16f..db7cb3a 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -7,7 +7,16 @@ import type { Comment as CommentSchema, WithContext } from 'schema-dts'; import type { SingleComment } from '../../../types'; import { useSettings } from '../../../utils/hooks'; import { Button, Link, Time } from '../../atoms'; -import { MetaList } from '../../molecules'; +import { + Card, + CardActions, + CardBody, + CardCover, + CardFooter, + CardHeader, + CardMeta, + CardTitle, +} from '../../molecules'; import { CommentForm, type CommentFormProps } from '../forms'; import styles from './comment.module.scss'; @@ -103,9 +112,6 @@ export const UserComment: FC = ({ text: content, }; - const commentWrapperClass = `${styles.wrapper} ${styles['wrapper--comment']}`; - const formWrapperClass = `${styles.wrapper} ${styles['wrapper--form']}`; - return ( <>