From 50f1c501a87ef5f5650750dbeca797e833ec7c3a Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 14 Nov 2023 12:39:09 +0100 Subject: refactor(components): replace Sharing with SharingWidget component * all the widgets should have a coherent name * fix mailto uri * remove useless CSS * add tests --- src/components/organisms/widgets/index.ts | 2 +- .../organisms/widgets/sharing-widget/index.ts | 1 + .../sharing-widget/sharing-widget.stories.tsx | 50 ++++ .../widgets/sharing-widget/sharing-widget.test.tsx | 167 +++++++++++++ .../widgets/sharing-widget/sharing-widget.tsx | 161 +++++++++++++ .../organisms/widgets/sharing.module.scss | 8 - .../organisms/widgets/sharing.stories.tsx | 61 ----- src/components/organisms/widgets/sharing.test.tsx | 24 -- src/components/organisms/widgets/sharing.tsx | 259 --------------------- 9 files changed, 380 insertions(+), 353 deletions(-) create mode 100644 src/components/organisms/widgets/sharing-widget/index.ts create mode 100644 src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx create mode 100644 src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx create mode 100644 src/components/organisms/widgets/sharing-widget/sharing-widget.tsx delete mode 100644 src/components/organisms/widgets/sharing.module.scss delete mode 100644 src/components/organisms/widgets/sharing.stories.tsx delete mode 100644 src/components/organisms/widgets/sharing.test.tsx delete mode 100644 src/components/organisms/widgets/sharing.tsx (limited to 'src/components/organisms/widgets') 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-widget/sharing-widget.stories.tsx b/src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx new file mode 100644 index 0000000..3e3cb68 --- /dev/null +++ b/src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx @@ -0,0 +1,50 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../../atoms'; +import { SharingWidget } from './sharing-widget'; + +/** + * SharingWidget - Storybook Meta + */ +export default { + title: 'Organisms/Widgets/Sharing', + component: SharingWidget, + argTypes: { + data: { + description: 'The page data.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + media: { + control: { + type: null, + }, + description: 'An array of active and ordered sharing medium.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +/** + * SharingWidget Stories - Sharing + */ +export const Sharing = Template.bind({}); +Sharing.args = { + data: { + excerpt: + 'Alias similique eius ducimus laudantium aspernatur. Est rem ut eum temporibus sit reprehenderit aut non molestias. Vel dolorem expedita labore quo inventore aliquid nihil nam. Possimus nobis enim quas corporis eos.', + title: 'Accusantium totam nostrum', + url: 'https://www.example.test', + }, + heading: Share, + 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( + {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( + corrupti} + 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( + corrupti} + 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( + corrupti} + 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( + corrupti} + 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( + corrupti} + 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( + corrupti} + 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( + maxime} + // @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, + data: Omit +) => { + 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 = { + '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 ( + + + {media.map((medium) => ( + + + + ))} + + + ); +}; + +/** + * 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.stories.tsx b/src/components/organisms/widgets/sharing.stories.tsx deleted file mode 100644 index d2be621..0000000 --- a/src/components/organisms/widgets/sharing.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Sharing as SharingWidget } from './sharing'; - -/** - * Sharing - Storybook Meta - */ -export default { - title: 'Organisms/Widgets', - 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: { - name: 'object', - required: true, - value: {}, - }, - }, - media: { - control: { - type: null, - }, - description: 'An array of active and ordered sharing medium.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -/** - * Widgets Stories - Sharing - */ -export const Sharing = Template.bind({}); -Sharing.args = { - data: { - excerpt: - 'Alias similique eius ducimus laudantium aspernatur. Est rem ut eum temporibus sit reprehenderit aut non molestias. Vel dolorem expedita labore quo inventore aliquid nihil nam. Possimus nobis enim quas corporis eos.', - title: 'Accusantium totam nostrum', - url: 'https://www.example.test', - }, - media: ['diaspora', 'facebook', 'linkedin', 'twitter', 'email'], -}; 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(); - 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 & { - /** - * 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 = ({ - 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) => ( -
  • - -
  • - )); - - return ( - - {widgetTitle} - - } - > -
      {getItems()}
    -
    - ); -}; -- cgit v1.2.3