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 --- .../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 ++++++++++++++++++++ 4 files changed, 379 insertions(+) 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 (limited to 'src/components/organisms/widgets/sharing-widget') 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); -- cgit v1.2.3