aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/organisms/widgets/sharing.module.scss10
-rw-r--r--src/components/organisms/widgets/sharing.stories.tsx78
-rw-r--r--src/components/organisms/widgets/sharing.test.tsx31
-rw-r--r--src/components/organisms/widgets/sharing.tsx190
4 files changed, 309 insertions, 0 deletions
diff --git a/src/components/organisms/widgets/sharing.module.scss b/src/components/organisms/widgets/sharing.module.scss
new file mode 100644
index 0000000..e06d4e3
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.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
new file mode 100644
index 0000000..be20b84
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.stories.tsx
@@ -0,0 +1,78 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import SharingWidget from './sharing';
+
+export default {
+ title: 'Organisms/Widgets',
+ component: SharingWidget,
+ argTypes: {
+ data: {
+ description: 'The page data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ expanded: {
+ control: {
+ type: null,
+ },
+ description: 'Default widget state (expanded or collapsed).',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ },
+ description: 'The heading level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ media: {
+ control: {
+ type: null,
+ },
+ description: 'An array of active and ordered sharing medium.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SharingWidget>;
+
+const Template: ComponentStory<typeof SharingWidget> = (args) => (
+ <IntlProvider locale="en">
+ <SharingWidget {...args} />
+ </IntlProvider>
+);
+
+export const Sharing = Template.bind({});
+Sharing.args = {
+ expanded: true,
+ 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',
+ },
+ level: 2,
+ media: ['diaspora', 'facebook', 'linkedin', 'twitter', 'email'],
+ title: 'Sharing',
+};
diff --git a/src/components/organisms/widgets/sharing.test.tsx b/src/components/organisms/widgets/sharing.test.tsx
new file mode 100644
index 0000000..265dbe1
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.test.tsx
@@ -0,0 +1,31 @@
+import { render, screen } from '@test-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']}
+ expanded={true}
+ title="Sharing"
+ level={2}
+ />
+ );
+ expect(
+ screen.getByRole('link', { name: 'Share on facebook' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('link', { name: 'Share on twitter' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('link', { name: 'Share on linkedin' })
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx
new file mode 100644
index 0000000..ccd3a21
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.tsx
@@ -0,0 +1,190 @@
+import SharingLink, {
+ type SharingMedium,
+} from '@components/atoms/links/sharing-link';
+import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
+import { VFC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './sharing.module.scss';
+
+export type SharingData = {
+ /**
+ * The content excerpt.
+ */
+ excerpt: string;
+ /**
+ * The content title.
+ */
+ title: string;
+ /**
+ * The content url.
+ */
+ url: string;
+};
+
+export type SharingProps = WidgetProps & {
+ /**
+ * 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.
+ */
+const Sharing: VFC<SharingProps> = ({ data, media, ...props }) => {
+ const intl = useIntl();
+
+ /**
+ * 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 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}`;
+ };
+
+ /**
+ * 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}`;
+ };
+
+ /**
+ * 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[] => {
+ return media.map((medium) => (
+ <li key={medium}>
+ <SharingLink medium={medium} url={getUrl(medium)} />
+ </li>
+ ));
+ };
+
+ return (
+ <Widget {...props}>
+ <ul className={styles.list}>{getItems()}</ul>
+ </Widget>
+ );
+};
+
+export default Sharing;