Skip to content

Commit

Permalink
feat: initial draft of social share functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelonah committed Jun 8, 2024
1 parent db33a38 commit e55c7cf
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REACT_APP_GEMINI_API_KEY=your_gemini_api_key
REACT_APP_OPENAI_API_KEY=your_chatgbpy_api_key
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const preview: Preview = {
<GlobalStyles theme={theme} />
<QueryClientProvider client={new QueryClient()}>
<GlobalStore>
<div style={{ width: '95vw', height: '100vh', backgroundColor: '#fff' }}>
<div style={{ width: '100vw', height: '100vh', backgroundColor: '#fff' }}>
<Story />
</div>
</GlobalStore>
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ x10 is a revolutionary search engine that leverages the power of Chrome, Bing, a
9. [Git Action](https://docs.github.com/en/actions)

10. [Storybook](https://storybook.js.org/)
11. [GEMINI API](https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=rest)
12. [OpenAI API](https://platform.openai.com/docs/overview)

## Architecture used

Expand Down Expand Up @@ -72,7 +74,7 @@ yarn run test:coverage``` script

- [Local URL](http://localhost:3000)

- [Staging URL](https://x10-staging.netlify.app/)
- [Staging URL](https://x10.netlify.app/)

- [Production URL](https://x10.dev)

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"react-query": "^3.39.3",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-share": "^5.1.0",
"styled-components": "^6.1.8",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.6.0",
Expand Down Expand Up @@ -87,14 +88,14 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-storybook": "^0.8.0",
"husky": "^8.0.0",
"husky-init": "^8.0.0",
"msw": "^2.2.4",
"prettier": "3.2.5",
"prop-types": "^15.8.1",
"storybook": "^8.0.0",
"typescript": "^4.9.5",
"webpack": "^5.90.3",
"husky": "^8.0.0",
"husky-init": "^8.0.0"
"webpack": "^5.90.3"
},
"eslintConfig": {
"extends": [
Expand Down
9 changes: 9 additions & 0 deletions src/design-system/global-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ const GlobalStyles = createGlobalStyle<Theme>`
textarea {
font-size: 1rem;
}
/**
* Override the default style of storybook tool
*/
.sb-show-main.sb-main-padded {
padding:0 !important;
}
`;

export { SkipToMainContent, GlobalStyles, theme };
100 changes: 100 additions & 0 deletions src/shared/components/social-share/index.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';

import { Share2Icon, Link2Icon } from '@radix-ui/react-icons';
import {
FacebookShareButton,
TwitterShareButton,
EmailShareButton,
WhatsappShareButton,
FacebookIcon,
XIcon,
WhatsappIcon,
EmailIcon,
} from 'react-share';

import { useBoolean, useCopyToKeyboard, COPY_STATUS } from 'shared/hooks';

import {
ShareButton,
Modal,
Heading,
Body,
Subtitle,
CloseButton,
HeadingRow1,
Footer,
LinkWrapper,
NeutralStatus,
SuccessStatus,
FailedStatus,
} from './index.styles';

const iconProps = {
size: 40,
borderRadius: 25,
};

type SocialSharePropTypes = {
url: string;
title: string;
};

const STATUS_NODES = {
[COPY_STATUS.NOT_COPIED]: NeutralStatus,
[COPY_STATUS.COPIED]: SuccessStatus,
[COPY_STATUS.FAILED]: FailedStatus,
};

export function SocialShare({ url, title }: SocialSharePropTypes) {
const [open, { toggle }] = useBoolean();
const { copyText, status, statusText } = useCopyToKeyboard(url);

const StatusNode = STATUS_NODES[status];

return (
<>
<ShareButton onClick={toggle} className="share-btn">
<Share2Icon />
<span>Share</span>
</ShareButton>

<Modal open={open} onClose={toggle} closeOnClickOutside>
<Heading>
<HeadingRow1>
<p>Share</p>
<CloseButton>Cancel</CloseButton>
</HeadingRow1>
<Subtitle>Images maybe subject to copyright.</Subtitle>
</Heading>

<Body>
<FacebookShareButton url={url} title={title}>
<FacebookIcon {...iconProps} />
</FacebookShareButton>
<WhatsappShareButton url={url}>
<WhatsappIcon {...iconProps} />
</WhatsappShareButton>
<TwitterShareButton url={url} title={title}>
<XIcon {...iconProps} />
</TwitterShareButton>
<EmailShareButton
url={url}
subject={title}
body="Sent from x10 search engine"
title={title}
>
<EmailIcon {...iconProps} />
</EmailShareButton>
</Body>

<Footer onClick={copyText}>
<StatusNode>{statusText}</StatusNode>
<LinkWrapper>
<span>{url}</span>
<Link2Icon role="button" />
</LinkWrapper>
</Footer>
</Modal>
</>
);
}
14 changes: 14 additions & 0 deletions src/shared/components/social-share/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { StoryFn, Meta } from '@storybook/react';

import { SocialShare } from './index.component';

export default {
title: 'Components/SocialShare',
component: SocialShare,
} as Meta<typeof SocialShare>;

export const Primary: StoryFn<typeof SocialShare> = () => (
<SocialShare title="View Emmanuel Onah Page" url="https://emmanuelonah.com" />
);
109 changes: 109 additions & 0 deletions src/shared/components/social-share/index.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import styled from 'styled-components';

import { ModalWrapper } from 'shared/components';

const ShareButton = styled.button`
background-color: #394457;
color: white;
border: none;
align-items: center;
border-radius: 36px;
cursor: pointer;
display: flex;
justify-content: center;
min-height: 30px;
min-width: 150px;
padding: 1rem;
& span {
padding-left: 0.5rem;
}
`;

const Modal = styled(ModalWrapper)`
position: fix;
bottom: 0;
right: 0;
left: 0;
margin: 0 auto;
background-color: #303134;
border-radius: 10px 10px 0 0;
min-height: 300px;
width: 100%;
`;

const Heading = styled.div`
color: rgb(232, 234, 237);
border-bottom: 1px solid rgb(60, 64, 67);
`;

const HeadingRow1 = styled.div`
display: flex;
justify-content: space-between;
padding: 1rem 1rem 0 1rem;
`;

const Subtitle = styled.p`
color: rgb(154, 160, 166);
font-size: 12px;
padding: 0 1rem 0.5rem 1rem;
`;

const CloseButton = styled.button`
color: rgb(232, 234, 237);
background-color: transparent;
border: none;
`;

const Body = styled.div`
padding: 1rem;
display: flex;
justify-content: space-between;
`;

const Footer = styled.div`
cursor: pointer;
border-top: 1px solid rgb(60, 64, 67);
color: rgb(154, 160, 166);
font-size: 12px;
padding: 1rem;
`;

const LinkWrapper = styled.div`
border-bottom: 2px solid rgb(60, 64, 67);
display: flex;
align-items: center;
padding-bottom: 0.5rem;
& span {
text-overflow: ellipsis;
padding-right: 0.5rem;
}
`;

const NeutralStatus = styled.p`
color: white;
`;

const SuccessStatus = styled.p`
color: green;
`;

const FailedStatus = styled.p`
color: red;
`;

export {
ShareButton,
Modal,
Heading,
Body,
Subtitle,
CloseButton,
HeadingRow1,
Footer,
LinkWrapper,
NeutralStatus,
SuccessStatus,
FailedStatus,
};
1 change: 1 addition & 0 deletions src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useBoolean';
export * from './useForceUpdate';
export * from './useComposeRefs';
export * from './useCopyToKeyboard';
68 changes: 68 additions & 0 deletions src/shared/hooks/useCopyToKeyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { renderHook, act } from 'test';

import { useCopyToKeyboard, COPY_STATUS } from './useCopyToKeyboard';

Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
},
});

describe('useCopyToKeyboard', () => {
beforeEach(jest.clearAllMocks);

it('should initially have NOT_COPIED status and corresponding text', () => {
const { result } = renderHook(() => useCopyToKeyboard('Test text'));

expect(result.current.status).toBe(COPY_STATUS.NOT_COPIED);
expect(result.current.statusText).toBe('Tap to copy link');
});

it('should change status to COPIED and show corresponding text on successful copy', async () => {
(navigator.clipboard.writeText as jest.Mock).mockResolvedValueOnce(null);
const { result } = renderHook(() => useCopyToKeyboard('Test text'));

await act(async () => {
await result.current.copyText();
});

expect(result.current.status).toBe(COPY_STATUS.COPIED);
expect(result.current.statusText).toBe('Link copied');
});

it('should change status to FAILED and show corresponding text on copy failure', async () => {
(navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error('Failed to copy'));
const { result } = renderHook(() => useCopyToKeyboard('Test text'));

await act(async () => {
await result.current.copyText();
});

expect(result.current.status).toBe(COPY_STATUS.FAILED);
expect(result.current.statusText).toBe('Failed to copy link. Retry?');
});

it('should copy text to clipboard on Ctrl+C (or Cmd+C on Mac) keydown', async () => {
const { result } = renderHook(() => useCopyToKeyboard('Test text'));

const event = new KeyboardEvent('keydown', { key: 'c', ctrlKey: true });
document.dispatchEvent(event);

await act(async () => {});

expect(navigator.clipboard.writeText as jest.Mock).toHaveBeenCalledWith('Test text');
expect(result.current.status).toBe(COPY_STATUS.COPIED);
});

it('should not change status on key press other than Ctrl+C (or Cmd+C on Mac)', async () => {
const { result } = renderHook(() => useCopyToKeyboard('Test text'));

const event = new KeyboardEvent('keydown', { key: 'a' });
document.dispatchEvent(event);

await act(async () => {});

expect(navigator.clipboard.writeText as jest.Mock).not.toHaveBeenCalled();
expect(result.current.status).toBe(COPY_STATUS.NOT_COPIED);
});
});
Loading

0 comments on commit e55c7cf

Please sign in to comment.