diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-12 16:09:21 +0200 | 
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-12 16:09:21 +0200 | 
| commit | 27ff3104aabed240470d351bda02095d8169501f (patch) | |
| tree | e4b43947c1150201067d40622b52b65bd19f01a2 /src/components/organisms | |
| parent | ff3a251e75fafce7d95177010401483127973313 (diff) | |
chore: add a Summary component
Diffstat (limited to 'src/components/organisms')
| -rw-r--r-- | src/components/organisms/layout/summary.module.scss | 84 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.stories.tsx | 114 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.test.tsx | 85 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.tsx | 105 | 
4 files changed, 388 insertions, 0 deletions
| diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss new file mode 100644 index 0000000..5da0a18 --- /dev/null +++ b/src/components/organisms/layout/summary.module.scss @@ -0,0 +1,84 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { +  @include mix.media("screen") { +    @include mix.dimensions("xs") { +      padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md); +      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); +    } + +    @include mix.dimensions("sm") { +      display: grid; +      grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); +      grid-template-rows: repeat(3, max-content); +      column-gap: var(--spacing-md); +    } +  } +} + +.cover { +  width: auto; +  max-height: fun.convert-px(100); +  max-width: 100%; +  border: fun.convert-px(1) solid var(--color-border); + +  @include mix.media("screen") { +    @include mix.dimensions("sm") { +      grid-column: 2; +      grid-row: 1; +    } +  } +} + +.header { +  @include mix.media("screen") { +    @include mix.dimensions("sm") { +      grid-column: 1; +      grid-row: 1; +      align-self: center; +    } +  } +} + +.body { +  @include mix.media("screen") { +    @include mix.dimensions("sm") { +      grid-column: 1; +      grid-row: 2; +    } +  } +} + +.footer { +  @include mix.media("screen") { +    @include mix.dimensions("sm") { +      grid-column: 2; +      grid-row: 2 / 4; +    } +  } +} + +.title { +  background: none; +  text-shadow: none; +} + +.read-more { +  display: flex; +  flex-flow: row nowrap; +  column-gap: var(--spacing-xs); +  width: max-content; +  margin: var(--spacing-sm) 0; +} + +.meta { +  font-size: var(--font-size-sm); +} diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx new file mode 100644 index 0000000..5214d70 --- /dev/null +++ b/src/components/organisms/layout/summary.stories.tsx @@ -0,0 +1,114 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import SummaryComponent from './summary'; + +export default { +  title: 'Organisms/Layout', +  component: SummaryComponent, +  args: { +    titleLevel: 2, +  }, +  argTypes: { +    cover: { +      description: 'The cover data.', +      table: { +        category: 'Options', +      }, +      type: { +        name: 'object', +        required: false, +        value: {}, +      }, +    }, +    excerpt: { +      control: { +        type: 'text', +      }, +      description: 'The page excerpt.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    meta: { +      description: 'The page metadata.', +      type: { +        name: 'object', +        required: true, +        value: {}, +      }, +    }, +    title: { +      control: { +        type: 'text', +      }, +      description: 'The page title', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    titleLevel: { +      control: { +        type: 'number', +      }, +      description: 'The page title level (hn)', +      table: { +        category: 'Options', +        defaultValue: { summary: 2 }, +      }, +      type: { +        name: 'number', +        required: false, +      }, +    }, +    url: { +      control: { +        type: 'text', +      }, +      description: 'The page url.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +  }, +} as ComponentMeta<typeof SummaryComponent>; + +const Template: ComponentStory<typeof SummaryComponent> = (args) => ( +  <IntlProvider locale="en"> +    <SummaryComponent {...args} /> +  </IntlProvider> +); + +const meta = { +  publication: { name: 'Published on:', value: 'April 11th 2022' }, +  readingTime: { name: 'Reading time:', value: '5 minutes' }, +  categories: { +    name: 'Categories:', +    value: [ +      <a key="cat-1" href="#"> +        Cat 1 +      </a>, +      <a key="cat-2" href="#"> +        Cat 2 +      </a>, +    ], +  }, +  comments: { name: 'Comments:', value: '1 comment' }, +}; + +export const Summary = Template.bind({}); +Summary.args = { +  cover: { +    alt: 'A cover', +    height: 480, +    url: 'http://placeimg.com/640/480', +    width: 640, +  }, +  excerpt: +    'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.', +  meta, +  title: 'Odio odit necessitatibus', +  url: '#', +}; diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx new file mode 100644 index 0000000..ce87c0c --- /dev/null +++ b/src/components/organisms/layout/summary.test.tsx @@ -0,0 +1,85 @@ +import { render, screen } from '@test-utils'; +import Summary from './summary'; + +const cover = { +  alt: 'A cover', +  height: 480, +  url: 'http://placeimg.com/640/480', +  width: 640, +}; + +const excerpt = +  'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.'; + +const meta = { +  publication: { name: 'Published on:', value: 'April 11th 2022' }, +  readingTime: { name: 'Reading time:', value: '5 minutes' }, +  categories: { +    name: 'Categories:', +    value: [ +      <a key="cat-1" href="#"> +        Cat 1 +      </a>, +      <a key="cat-2" href="#"> +        Cat 2 +      </a>, +    ], +  }, +  comments: { name: 'Comments:', value: '1 comment' }, +}; + +const title = 'Odio odit necessitatibus'; + +const url = '#'; + +describe('Summary', () => { +  it('renders a title wrapped in a h2 element', () => { +    render( +      <Summary +        excerpt={excerpt} +        meta={meta} +        title={title} +        titleLevel={2} +        url={url} +      /> +    ); +    expect( +      screen.getByRole('heading', { level: 2, name: title }) +    ).toBeInTheDocument(); +  }); + +  it('renders an excerpt', () => { +    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />); +    expect(screen.getByText(excerpt)).toBeInTheDocument(); +  }); + +  it('renders a cover', () => { +    render( +      <Summary +        cover={cover} +        excerpt={excerpt} +        meta={meta} +        title={title} +        url={url} +      /> +    ); +    expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument(); +  }); + +  it('renders a link to the full post', () => { +    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />); +    expect(screen.getByRole('link', { name: title })).toBeInTheDocument(); +  }); + +  it('renders a read more link', () => { +    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />); +    expect( +      screen.getByRole('link', { name: `Read more about ${title}` }) +    ).toBeInTheDocument(); +  }); + +  it('renders some meta', () => { +    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />); +    expect(screen.getByText(meta.publication.name)).toBeInTheDocument(); +  }); +}); diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx new file mode 100644 index 0000000..3624e5d --- /dev/null +++ b/src/components/organisms/layout/summary.tsx @@ -0,0 +1,105 @@ +import ButtonLink from '@components/atoms/buttons/button-link'; +import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; +import Arrow from '@components/atoms/icons/arrow'; +import Link from '@components/atoms/links/link'; +import ResponsiveImage from '@components/molecules/images/responsive-image'; +import Meta, { type MetaItem } from '@components/molecules/layout/meta'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './summary.module.scss'; + +export type Cover = { +  alt: string; +  height: number; +  url: string; +  width: number; +}; + +export type RequiredMetaKey = 'publication'; + +export type RequiredMeta = { +  [key in RequiredMetaKey]: MetaItem; +}; + +export type OptionalMetaKey = +  | 'author' +  | 'categories' +  | 'comments' +  | 'readingTime' +  | 'update'; + +export type OptionalMeta = { +  [key in OptionalMetaKey]?: MetaItem; +}; + +export type Meta = RequiredMeta & OptionalMeta; + +export type SummaryProps = { +  cover?: Cover; +  excerpt: string; +  meta: Meta; +  title: string; +  titleLevel?: HeadingLevel; +  url: string; +}; + +/** + * Summary component + * + * Render a page summary. + */ +const Summary: VFC<SummaryProps> = ({ +  cover, +  excerpt, +  meta, +  title, +  titleLevel = 2, +  url, +}) => { +  const intl = useIntl(); + +  return ( +    <article className={styles.wrapper}> +      {cover && ( +        <ResponsiveImage +          alt={cover.alt} +          src={cover.url} +          width={cover.width} +          height={cover.height} +          className={styles.cover} +        /> +      )} +      <header className={styles.header}> +        <Link href={url}> +          <Heading level={titleLevel} className={styles.title}> +            {title} +          </Heading> +        </Link> +      </header> +      <div className={styles.body}> +        {excerpt} +        <ButtonLink target={url} className={styles['read-more']}> +          {intl.formatMessage( +            { +              defaultMessage: 'Read more<a11y> about {title}</a11y>', +              description: 'Summary: read more link', +              id: 'Zpgv+f', +            }, +            { +              title, +              a11y: (chunks: string) => ( +                <span className="screen-reader-text">{chunks}</span> +              ), +            } +          )} +          <Arrow direction="right" /> +        </ButtonLink> +      </div> +      <footer className={styles.footer}> +        <Meta data={meta} layout="column" className={styles.meta} /> +      </footer> +    </article> +  ); +}; + +export default Summary; | 
