diff options
Diffstat (limited to 'src/components/atoms/links/social-link')
5 files changed, 232 insertions, 0 deletions
diff --git a/src/components/atoms/links/social-link/index.ts b/src/components/atoms/links/social-link/index.ts new file mode 100644 index 0000000..9ac1b3b --- /dev/null +++ b/src/components/atoms/links/social-link/index.ts @@ -0,0 +1 @@ +export * from './social-link'; diff --git a/src/components/atoms/links/social-link/social-link.module.scss b/src/components/atoms/links/social-link/social-link.module.scss new file mode 100644 index 0000000..1aeab91 --- /dev/null +++ b/src/components/atoms/links/social-link/social-link.module.scss @@ -0,0 +1,38 @@ +@use "../../../../styles/abstracts/functions" as fun; + +.link { + display: flex; + width: var(--link-size, #{fun.convert-px(60)}); + height: var(--link-size, #{fun.convert-px(60)}); + box-shadow: + fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) var(--color-shadow), + fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-1) + var(--color-shadow), + fun.convert-px(3) fun.convert-px(4) fun.convert-px(4) fun.convert-px(-3) + var(--color-shadow), + 0 0 0 0 var(--color-shadow); + transition: all 0.25s linear 0s; + + &:hover, + &:focus { + box-shadow: + fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) var(--color-shadow), + fun.convert-px(1) fun.convert-px(1) fun.convert-px(2) fun.convert-px(-1) + var(--color-shadow-light), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) fun.convert-px(-4) + var(--color-shadow-light), + fun.convert-px(6) fun.convert-px(6) fun.convert-px(10) fun.convert-px(-3) + var(--color-shadow); + transform: scale(1.15); + } + + &:focus { + outline: var(--color-primary) dashed fun.convert-px(2); + } + + &:active { + box-shadow: 0 0 0 0 var(--color-shadow); + outline: none; + transform: scale(0.9); + } +} diff --git a/src/components/atoms/links/social-link/social-link.stories.tsx b/src/components/atoms/links/social-link/social-link.stories.tsx new file mode 100644 index 0000000..cfb4ac7 --- /dev/null +++ b/src/components/atoms/links/social-link/social-link.stories.tsx @@ -0,0 +1,66 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { SocialLink } from './social-link'; + +/** + * SocialLink - Storybook Meta + */ +export default { + title: 'Atoms/Links/Social', + component: SocialLink, + argTypes: { + url: { + control: { + type: null, + }, + description: 'Social profile url.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof SocialLink>; + +const Template: ComponentStory<typeof SocialLink> = (args) => ( + <SocialLink {...args} /> +); + +/** + * Social Link Stories - Github + */ +export const Github = Template.bind({}); +Github.args = { + icon: 'Github', + label: 'Github profile', + url: '#', +}; + +/** + * Social Link Stories - Gitlab + */ +export const Gitlab = Template.bind({}); +Gitlab.args = { + icon: 'Gitlab', + label: 'Gitlab profile', + url: '#', +}; + +/** + * Social Link Stories - LinkedIn + */ +export const LinkedIn = Template.bind({}); +LinkedIn.args = { + icon: 'LinkedIn', + label: 'LinkedIn profile', + url: '#', +}; + +/** + * Social Link Stories - Twitter + */ +export const Twitter = Template.bind({}); +Twitter.args = { + icon: 'Twitter', + label: 'Twitter profile', + url: '#', +}; diff --git a/src/components/atoms/links/social-link/social-link.test.tsx b/src/components/atoms/links/social-link/social-link.test.tsx new file mode 100644 index 0000000..9129c27 --- /dev/null +++ b/src/components/atoms/links/social-link/social-link.test.tsx @@ -0,0 +1,62 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { SocialLink } from './social-link'; + +/** + * Next.js mock images to use next/image component. So for now, I need to mock + * the svg files manually. + */ +jest.mock('@assets/images/social-media/github.svg', () => 'svg-file'); +jest.mock('@assets/images/social-media/gitlab.svg', () => 'svg-file'); +jest.mock('@assets/images/social-media/linkedin.svg', () => 'svg-file'); +jest.mock('@assets/images/social-media/twitter.svg', () => 'svg-file'); + +describe('SocialLink', () => { + it('render a Github social link', () => { + const label = 'sed ea impedit'; + const target = '/voluptas'; + + render(<SocialLink icon="Github" label={label} url={target} />); + + expect(rtlScreen.getByRole('link', { name: label })).toHaveAttribute( + 'href', + target + ); + }); + + it('render a Gitlab social link', () => { + const label = 'rerum velit asperiores'; + const target = '/enim'; + + render(<SocialLink icon="Gitlab" label={label} url={target} />); + + expect(rtlScreen.getByRole('link', { name: label })).toHaveAttribute( + 'href', + target + ); + }); + + it('render a LinkedIn social link', () => { + const label = 'in dolores sed'; + const target = '/ut'; + + render(<SocialLink icon="LinkedIn" label={label} url={target} />); + + expect(rtlScreen.getByRole('link', { name: label })).toHaveAttribute( + 'href', + target + ); + }); + + it('render a Twitter social link', () => { + const label = 'voluptatibus temporibus expedita'; + const target = '/magni'; + + render(<SocialLink icon="Twitter" label={label} url={target} />); + + expect(rtlScreen.getByRole('link', { name: label })).toHaveAttribute( + 'href', + target + ); + }); +}); diff --git a/src/components/atoms/links/social-link/social-link.tsx b/src/components/atoms/links/social-link/social-link.tsx new file mode 100644 index 0000000..1da1e7d --- /dev/null +++ b/src/components/atoms/links/social-link/social-link.tsx @@ -0,0 +1,65 @@ +import type { AnchorHTMLAttributes, FC } from 'react'; +import GithubIcon from '../../../../assets/images/social-media/github.svg'; +import GitlabIcon from '../../../../assets/images/social-media/gitlab.svg'; +import LinkedInIcon from '../../../../assets/images/social-media/linkedin.svg'; +import TwitterIcon from '../../../../assets/images/social-media/twitter.svg'; +import styles from './social-link.module.scss'; + +export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter'; + +export type SocialLinkProps = Omit< + AnchorHTMLAttributes<HTMLAnchorElement>, + 'aria-label' | 'children' | 'href' +> & { + /** + * The social link icon. + */ + icon: SocialWebsite; + /** + * An accessible label for the link. + */ + label: string; + /** + * The social profile url. + */ + url: string; +}; + +/** + * SocialLink component + * + * Render a social icon link. + */ +export const SocialLink: FC<SocialLinkProps> = ({ + className = '', + icon, + label, + url, + ...props +}) => { + const linkClass = `${styles.link} ${className}`; + + /** + * Retrieve a social link icon by id. + * @param {string} id - The social website id. + */ + const getIcon = (id: string) => { + switch (id) { + case 'Github': + return <GithubIcon aria-hidden className={styles.icon} />; + case 'Gitlab': + return <GitlabIcon aria-hidden className={styles.icon} />; + case 'LinkedIn': + return <LinkedInIcon aria-hidden className={styles.icon} />; + case 'Twitter': + default: + return <TwitterIcon aria-hidden className={styles.icon} />; + } + }; + + return ( + <a {...props} aria-label={label} className={linkClass} href={url}> + {getIcon(icon)} + </a> + ); +}; |
