diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-14 12:39:09 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-14 12:50:32 +0100 |
| commit | 50f1c501a87ef5f5650750dbeca797e833ec7c3a (patch) | |
| tree | f1f55092696c7261eaa7f9f9a9338253ede65c2b /src/components | |
| parent | fb29b0f017fae162ffa7ad6bdfc80099346802de (diff) | |
refactor(components): replace Sharing with SharingWidget component
* all the widgets should have a coherent name
* fix mailto uri
* remove useless CSS
* add tests
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/organisms/widgets/index.ts | 2 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing-widget/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx (renamed from src/components/organisms/widgets/sharing.stories.tsx) | 23 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx | 167 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing-widget/sharing-widget.tsx | 161 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing.module.scss | 8 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing.test.tsx | 24 | ||||
| -rw-r--r-- | src/components/organisms/widgets/sharing.tsx | 259 | ||||
| -rw-r--r-- | src/components/templates/page/page-layout.stories.tsx | 8 |
9 files changed, 341 insertions, 312 deletions
diff --git a/src/components/organisms/widgets/index.ts b/src/components/organisms/widgets/index.ts index 03f845f..2286898 100644 --- a/src/components/organisms/widgets/index.ts +++ b/src/components/organisms/widgets/index.ts @@ -1,5 +1,5 @@ export * from './image-widget'; export * from './links-list-widget'; -export * from './sharing'; +export * from './sharing-widget'; export * from './social-media-widget'; export * from './table-of-contents'; diff --git a/src/components/organisms/widgets/sharing-widget/index.ts b/src/components/organisms/widgets/sharing-widget/index.ts new file mode 100644 index 0000000..dd78023 --- /dev/null +++ b/src/components/organisms/widgets/sharing-widget/index.ts @@ -0,0 +1 @@ +export * from './sharing-widget'; diff --git a/src/components/organisms/widgets/sharing.stories.tsx b/src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx index d2be621..3e3cb68 100644 --- a/src/components/organisms/widgets/sharing.stories.tsx +++ b/src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx @@ -1,26 +1,14 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Sharing as SharingWidget } from './sharing'; +import { Heading } from '../../../atoms'; +import { SharingWidget } from './sharing-widget'; /** - * Sharing - Storybook Meta + * SharingWidget - Storybook Meta */ export default { - title: 'Organisms/Widgets', + title: 'Organisms/Widgets/Sharing', component: SharingWidget, argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the sharing links list.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, data: { description: 'The page data.', type: { @@ -47,7 +35,7 @@ const Template: ComponentStory<typeof SharingWidget> = (args) => ( ); /** - * Widgets Stories - Sharing + * SharingWidget Stories - Sharing */ export const Sharing = Template.bind({}); Sharing.args = { @@ -57,5 +45,6 @@ Sharing.args = { title: 'Accusantium totam nostrum', url: 'https://www.example.test', }, + heading: <Heading level={3}>Share</Heading>, media: ['diaspora', 'facebook', 'linkedin', 'twitter', 'email'], }; diff --git a/src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx b/src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx new file mode 100644 index 0000000..b8bc702 --- /dev/null +++ b/src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx @@ -0,0 +1,167 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../tests/utils'; +import { Heading, type SharingMedium } from '../../../atoms'; +import { SharingWidget, type SharingData } from './sharing-widget'; + +const data: SharingData = { + excerpt: 'A post excerpt', + title: 'A post title', + url: 'https://sharing-website.test', +}; + +describe('SharingWidget', () => { + it('renders the widget heading and a list of links', () => { + const heading = 'dolorem necessitatibus voluptatem'; + const headingLvl = 3; + const media = ['facebook', 'twitter'] satisfies SharingMedium[]; + + render( + <SharingWidget + data={data} + heading={<Heading level={headingLvl}>{heading}</Heading>} + media={media} + /> + ); + + expect( + rtlScreen.getByRole('heading', { level: headingLvl }) + ).toHaveTextContent(heading); + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(media.length); + expect(rtlScreen.getAllByRole('link')).toHaveLength(media.length); + }); + + it('can render a link to share on Diaspora', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['diaspora']} + /> + ); + + const link = rtlScreen.getByRole('link'); + + expect(link).toHaveTextContent('Share on Diaspora'); + expect(link).toHaveAttribute( + 'href', + `https://share.diasporafoundation.org/?title=${encodeURIComponent( + data.title + )}&url=${encodeURIComponent(data.url)}` + ); + }); + + it('can render a link to share on Facebook', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['facebook']} + /> + ); + + const link = rtlScreen.getByRole('link'); + + expect(link).toHaveTextContent('Share on Facebook'); + expect(link).toHaveAttribute( + 'href', + `https://www.facebook.com/sharer/sharer.php?$u=${encodeURIComponent( + data.url + )}` + ); + }); + + it('can render a link to share on Journal du Hacker', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['journal-du-hacker']} + /> + ); + + const link = rtlScreen.getByRole('link'); + + expect(link).toHaveTextContent('Share on Journal du Hacker'); + expect(link).toHaveAttribute( + 'href', + `https://www.journalduhacker.net/stories/new?title=${encodeURIComponent( + data.title + )}&url=${encodeURIComponent(data.url)}` + ); + }); + + it('can render a link to share on LinkedIn', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['linkedin']} + /> + ); + + const link = rtlScreen.getByRole('link'); + + expect(link).toHaveTextContent('Share on LinkedIn'); + expect(link).toHaveAttribute( + 'href', + `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent( + data.url + )}` + ); + }); + + it('can render a link to share on Twitter', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['twitter']} + /> + ); + + const link = rtlScreen.getByRole('link'); + + expect(link).toHaveTextContent('Share on Twitter'); + expect(link).toHaveAttribute( + 'href', + `https://twitter.com/intent/tweet?text=${encodeURIComponent( + data.title + )}&url=${encodeURIComponent(data.url)}` + ); + }); + + it('can render a link to share by Email', () => { + render( + <SharingWidget + data={data} + heading={<Heading level={3}>corrupti</Heading>} + media={['email']} + /> + ); + + const link = rtlScreen.getByRole('link'); + const subject = `You should read ${data.title}`; + const body = `${data.excerpt}\n\nRead more here: ${data.url}`; + + expect(link).toHaveTextContent('Share by Email'); + expect(link).toHaveAttribute( + 'href', + `mailto:?body=${encodeURIComponent(body)}&subject=${encodeURIComponent( + subject + )}` + ); + }); + + it('throws an error when a medium is invalid', () => { + expect(() => + render( + <SharingWidget + data={data} + heading={<Heading level={3}>maxime</Heading>} + // @ts-expect-error -- Unsupported medium + media={['not-supported']} + /> + ) + ).toThrowError('Unsupported social media.'); + }); +}); diff --git a/src/components/organisms/widgets/sharing-widget/sharing-widget.tsx b/src/components/organisms/widgets/sharing-widget/sharing-widget.tsx new file mode 100644 index 0000000..afac177 --- /dev/null +++ b/src/components/organisms/widgets/sharing-widget/sharing-widget.tsx @@ -0,0 +1,161 @@ +import { type ForwardRefRenderFunction, forwardRef, useCallback } from 'react'; +import { useIntl } from 'react-intl'; +import { + List, + ListItem, + SharingLink, + type SharingMedium, +} from '../../../atoms'; +import { Collapsible, type CollapsibleProps } from '../../../molecules'; + +export type SharingData = { + /** + * The content excerpt. + */ + excerpt: string; + /** + * The content title. + */ + title: string; + /** + * The content url. + */ + url: string; +}; + +const getUrl = ( + medium: Exclude<SharingMedium, 'email'>, + data: Omit<SharingData, 'excerpt'> +) => { + const title = encodeURIComponent(data.title); + const url = encodeURIComponent(data.url); + + switch (medium) { + case 'diaspora': + return `https://share.diasporafoundation.org/?title=${title}&url=${url}`; + case 'facebook': + return `https://www.facebook.com/sharer/sharer.php?$u=${url}`; + case 'journal-du-hacker': + return `https://www.journalduhacker.net/stories/new?title=${title}&url=${url}`; + case 'linkedin': + return `https://www.linkedin.com/sharing/share-offsite/?url=${url}`; + case 'twitter': + return `https://twitter.com/intent/tweet?text=${title}&url=${url}`; + default: + throw new Error('Unsupported social media.'); + } +}; + +export type SharingWidgetProps = Omit< + CollapsibleProps, + 'children' | 'disablePadding' | 'hasBorders' +> & { + /** + * The page data to share. + */ + data: SharingData; + /** + * An ordered list of sharing medium to activate. + */ + media: SharingMedium[]; +}; + +const SharingWidgetWithRef: ForwardRefRenderFunction< + HTMLDivElement, + SharingWidgetProps +> = ({ data, media, ...props }, ref) => { + const intl = useIntl(); + const labels: Record<SharingMedium, string> = { + 'journal-du-hacker': intl.formatMessage({ + defaultMessage: 'Share on Journal du Hacker', + description: 'SharingWidget: Journal du Hacker sharing link', + id: 'Hclr0a', + }), + diaspora: intl.formatMessage({ + defaultMessage: 'Share on Diaspora', + description: 'SharingWidget: Diaspora sharing link', + id: '0f7fty', + }), + email: intl.formatMessage({ + defaultMessage: 'Share by Email', + description: 'SharingWidget: Email sharing link', + id: 'OWygWB', + }), + facebook: intl.formatMessage({ + defaultMessage: 'Share on Facebook', + description: 'SharingWidget: Facebook sharing link', + id: 'WzYUm5', + }), + linkedin: intl.formatMessage({ + defaultMessage: 'Share on LinkedIn', + description: 'SharingWidget: LinkedIn sharing link', + id: 'ofQPC+', + }), + twitter: intl.formatMessage({ + defaultMessage: 'Share on Twitter', + description: 'SharingWidget: Twitter sharing link', + id: 'QdBC6q', + }), + }; + + /** + * Build the mailto url from provided data. + * + * @returns {string} The mailto url with params. + */ + const buildEmailUrl = useCallback((): string => { + const readMore = intl.formatMessage({ + defaultMessage: 'Read more here:', + description: 'SharingWidget: content link prefix', + id: 'AsXE0d', + }); + const excerpt = data.excerpt + .replace(/<[^>]+>/gi, '') + .replaceAll(' ', ' '); + const body = `${excerpt}\n\n${readMore} ${data.url}`; + const subject = intl.formatMessage( + { + defaultMessage: 'You should read {title}', + description: 'SharingWidget: subject text', + id: 'BLq3+e', + }, + { title: data.title } + ); + + return `mailto:?body=${encodeURIComponent( + body + )}&subject=${encodeURIComponent(subject)}`; + }, [data, intl]); + + return ( + <Collapsible {...props} ref={ref}> + <List + hideMarker + isInline + // eslint-disable-next-line react/jsx-no-literals + spacing="xs" + > + {media.map((medium) => ( + <ListItem key={medium}> + <SharingLink + label={labels[medium]} + medium={medium} + url={ + medium === 'email' + ? buildEmailUrl() + : getUrl(medium, { title: data.title, url: data.url }) + } + /> + </ListItem> + ))} + </List> + </Collapsible> + ); +}; + +/** + * Sharing widget component + * + * Render a list of sharing links inside a widget. + */ +export const SharingWidget = forwardRef(SharingWidgetWithRef); diff --git a/src/components/organisms/widgets/sharing.module.scss b/src/components/organisms/widgets/sharing.module.scss deleted file mode 100644 index 24f6fc9..0000000 --- a/src/components/organisms/widgets/sharing.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.list { - display: flex; - flex-flow: row wrap; - gap: var(--spacing-xs); - margin: 0; - padding: 0 var(--spacing-2xs); - list-style-type: none; -} diff --git a/src/components/organisms/widgets/sharing.test.tsx b/src/components/organisms/widgets/sharing.test.tsx deleted file mode 100644 index c7211f0..0000000 --- a/src/components/organisms/widgets/sharing.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { Sharing, type SharingData } from './sharing'; - -const postData: SharingData = { - excerpt: 'A post excerpt', - title: 'A post title', - url: 'https://sharing-website.test', -}; - -describe('Sharing', () => { - it('renders a sharing widget', () => { - render(<Sharing data={postData} media={['facebook', 'twitter']} />); - expect( - rtlScreen.getByRole('link', { name: 'Share on Facebook' }) - ).toBeInTheDocument(); - expect( - rtlScreen.getByRole('link', { name: 'Share on Twitter' }) - ).toBeInTheDocument(); - expect( - rtlScreen.queryByRole('link', { name: 'Share on LinkedIn' }) - ).not.toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx deleted file mode 100644 index 47ec49d..0000000 --- a/src/components/organisms/widgets/sharing.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import { Heading, SharingLink, type SharingMedium } from '../../atoms'; -import { Collapsible, type CollapsibleProps } from '../../molecules'; -import styles from './sharing.module.scss'; - -/** - * Build the Diaspora sharing url with provided data. - * - * @param {string} title - The content title. - * @param {string} url - The content url. - * @returns {string} The Diaspora url. - */ -const buildDiasporaUrl = (title: string, url: string): string => { - const titleParam = `title=${encodeURI(title)}`; - const urlParam = `url=${encodeURI(url)}`; - return `https://share.diasporafoundation.org/?${titleParam}&${urlParam}`; -}; - -/** - * Build the Facebook sharing url with provided data. - * - * @param {string} url - The content url. - * @returns {string} The Facebook url. - */ -const buildFacebookUrl = (url: string): string => { - const urlParam = `u=${encodeURI(url)}`; - return `https://www.facebook.com/sharer/sharer.php?${urlParam}`; -}; - -/** - * Build the Journal du Hacker sharing url with provided data. - * - * @param {string} title - The content title. - * @param {string} url - The content url. - * @returns {string} The Journal du Hacker url. - */ -const buildJdHUrl = (title: string, url: string): string => { - const titleParam = `title=${encodeURI(title)}`; - const urlParam = `url=${encodeURI(url)}`; - return `https://www.journalduhacker.net/stories/new?${titleParam}&${urlParam}`; -}; - -/** - * Build the LinkedIn sharing url with provided data. - * - * @param {string} url - The content url. - * @returns {string} The LinkedIn url. - */ -const buildLinkedInUrl = (url: string): string => { - const urlParam = `url=${encodeURI(url)}`; - return `https://www.linkedin.com/sharing/share-offsite/?${urlParam}`; -}; - -/** - * Build the Twitter sharing url with provided data. - * - * @param {string} title - The content title. - * @param {string} url - The content url. - * @returns {string} The Twitter url. - */ -const buildTwitterUrl = (title: string, url: string): string => { - const titleParam = `text=${encodeURI(title)}`; - const urlParam = `url=${encodeURI(url)}`; - return `https://twitter.com/intent/tweet?${titleParam}&${urlParam}`; -}; - -export type SharingData = { - /** - * The content excerpt. - */ - excerpt: string; - /** - * The content title. - */ - title: string; - /** - * The content url. - */ - url: string; -}; - -export type SharingProps = Omit<CollapsibleProps, 'children' | 'heading'> & { - /** - * Set additional classnames to the sharing links list. - */ - className?: string; - /** - * The page data to share. - */ - data: SharingData; - /** - * A list of active and ordered sharing medium. - */ - media: SharingMedium[]; -}; - -/** - * Sharing widget component - * - * Render a list of sharing links inside a widget. - */ -export const Sharing: FC<SharingProps> = ({ - className = '', - data, - media, - ...props -}) => { - const listClass = `${styles.list} ${className}`; - const intl = useIntl(); - const widgetTitle = intl.formatMessage({ - defaultMessage: 'Share', - id: 'q3U6uI', - description: 'Sharing: widget title', - }); - - /** - * Build the mailto url from provided data. - * - * @param {string} excerpt - The content excerpt. - * @param {string} title - The content title. - * @param {string} url - The content url. - * @returns {string} The mailto url with params. - */ - const buildEmailUrl = ( - excerpt: string, - title: string, - url: string - ): string => { - const intro = intl.formatMessage({ - defaultMessage: 'Introduction:', - description: 'Sharing: email content prefix', - id: 'yfgMcl', - }); - const readMore = intl.formatMessage({ - defaultMessage: 'Read more here:', - description: 'Sharing: content link prefix', - id: 'UsQske', - }); - const body = `${intro}\n\n"${excerpt}"\n\n${readMore} ${url}`; - const bodyParam = excerpt ? `body=${encodeURI(body)}` : ''; - - const subject = intl.formatMessage( - { - defaultMessage: 'You should read {title}', - description: 'Sharing: subject text', - id: 'azgQuH', - }, - { title } - ); - const subjectParam = `subject=${encodeURI(subject)}`; - - return `mailto:?${bodyParam}&${subjectParam}`; - }; - - /** - * Retrieve the sharing label by medium id. - * - * @param {SharingMedium} medium - A sharing medium id. - * @returns {string} The sharing label. - */ - const getLabel = (medium: SharingMedium): string => { - switch (medium) { - case 'diaspora': - return intl.formatMessage({ - defaultMessage: 'Share on Diaspora', - description: 'Sharing: Diaspora sharing link', - id: 'oVLRW8', - }); - case 'email': - return intl.formatMessage({ - defaultMessage: 'Share by Email', - description: 'Sharing: Email sharing link', - id: '2ukj9H', - }); - case 'facebook': - return intl.formatMessage({ - defaultMessage: 'Share on Facebook', - description: 'Sharing: Facebook sharing link', - id: 'o0DAK4', - }); - case 'journal-du-hacker': - return intl.formatMessage({ - defaultMessage: 'Share on Journal du Hacker', - description: 'Sharing: Journal du Hacker sharing link', - id: 'vnbryZ', - }); - case 'linkedin': - return intl.formatMessage({ - defaultMessage: 'Share on LinkedIn', - description: 'Sharing: LinkedIn sharing link', - id: 'Y+DYja', - }); - case 'twitter': - default: - return intl.formatMessage({ - defaultMessage: 'Share on Twitter', - description: 'Sharing: Twitter sharing link', - id: 'NI5gXc', - }); - } - }; - - /** - * Retrieve the sharing url by medium id. - * - * @param {SharingMedium} medium - A sharing medium id. - * @returns {string} The sharing url. - */ - const getUrl = (medium: SharingMedium): string => { - const { excerpt, title, url } = data; - - switch (medium) { - case 'diaspora': - return buildDiasporaUrl(title, url); - case 'email': - return buildEmailUrl(excerpt, title, url); - case 'facebook': - return buildFacebookUrl(url); - case 'journal-du-hacker': - return buildJdHUrl(title, url); - case 'linkedin': - return buildLinkedInUrl(url); - case 'twitter': - return buildTwitterUrl(title, url); - default: - return '#'; - } - }; - - /** - * Get the sharing list items. - * - * @returns {JSX.Element[]} The sharing links wrapped with li element. - */ - const getItems = (): JSX.Element[] => - media.map((medium) => ( - <li key={medium}> - <SharingLink - label={getLabel(medium)} - medium={medium} - url={getUrl(medium)} - /> - </li> - )); - - return ( - <Collapsible - {...props} - heading={ - <Heading isFake level={3}> - {widgetTitle} - </Heading> - } - > - <ul className={listClass}>{getItems()}</ul> - </Collapsible> - ); -}; diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 20740db..9f0cce1 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -1,6 +1,6 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { ButtonLink, Heading, Link } from '../../atoms'; -import { LinksListWidget, PostsList, Sharing } from '../../organisms'; +import { LinksListWidget, PostsList, SharingWidget } from '../../organisms'; import { LayoutBase } from '../layout/layout.stories'; import { PageLayout as PageLayoutComponent } from './page-layout'; @@ -239,9 +239,10 @@ SinglePage.args = { </> ), widgets: [ - <Sharing + <SharingWidget key="sidebar2-widget1" data={{ excerpt: pageIntro, title: pageTitle, url: '#' }} + heading={<Heading level={3}>Share</Heading>} media={[ 'diaspora', 'email', @@ -330,9 +331,10 @@ Post.args = { </> ), widgets: [ - <Sharing + <SharingWidget key="sidebar2-widget1" data={{ excerpt: pageIntro, title: pageTitle, url: '#' }} + heading={<Heading level={3}>Share</Heading>} media={[ 'diaspora', 'email', |
