aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/widgets/sharing-widget
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/organisms/widgets/sharing-widget')
-rw-r--r--src/components/organisms/widgets/sharing-widget/index.ts1
-rw-r--r--src/components/organisms/widgets/sharing-widget/sharing-widget.stories.tsx50
-rw-r--r--src/components/organisms/widgets/sharing-widget/sharing-widget.test.tsx167
-rw-r--r--src/components/organisms/widgets/sharing-widget/sharing-widget.tsx161
4 files changed, 379 insertions, 0 deletions
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<typeof SharingWidget>;
+
+const Template: ComponentStory<typeof SharingWidget> = (args) => (
+ <SharingWidget {...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: <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('&nbsp;', ' ');
+ 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);