aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-14 12:39:09 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-14 12:50:32 +0100
commit50f1c501a87ef5f5650750dbeca797e833ec7c3a (patch)
treef1f55092696c7261eaa7f9f9a9338253ede65c2b /src/components
parentfb29b0f017fae162ffa7ad6bdfc80099346802de (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.ts2
-rw-r--r--src/components/organisms/widgets/sharing-widget/index.ts1
-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.tsx167
-rw-r--r--src/components/organisms/widgets/sharing-widget/sharing-widget.tsx161
-rw-r--r--src/components/organisms/widgets/sharing.module.scss8
-rw-r--r--src/components/organisms/widgets/sharing.test.tsx24
-rw-r--r--src/components/organisms/widgets/sharing.tsx259
-rw-r--r--src/components/templates/page/page-layout.stories.tsx8
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('&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);
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',