diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index de2cab35..e2708e2a 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -23,6 +23,7 @@ import { Tooltip, Typography } from '@mui/material'; import useAppStore from '../../store/useStore'; import { StorageService } from '../../services/StorageService'; import { IUser } from '../../utils/types'; +import { BsBarChartFill } from 'react-icons/bs'; const Sidebar = () => { const { guildInfoByDiscord } = useAppStore(); @@ -62,6 +63,15 @@ const Sidebar = () => { /> ), }, + { + name: 'Growth', + path: '/growth', + icon: ( + + ), + }, { name: 'Settings', path: '/settings', diff --git a/src/components/layouts/xs/SidebarXs.tsx b/src/components/layouts/xs/SidebarXs.tsx index 7cf21649..2a6e3cdc 100644 --- a/src/components/layouts/xs/SidebarXs.tsx +++ b/src/components/layouts/xs/SidebarXs.tsx @@ -26,6 +26,7 @@ import useAppStore from '../../../store/useStore'; import { StorageService } from '../../../services/StorageService'; import { IUser } from '../../../utils/types'; import { conf } from '../../../configs'; +import { BsBarChartFill } from 'react-icons/bs'; const Sidebar = () => { const { guildInfoByDiscord } = useAppStore(); @@ -65,6 +66,15 @@ const Sidebar = () => { /> ), }, + { + name: 'Growth', + path: '/growth', + icon: ( + + ), + }, { name: 'Settings', path: '/settings', diff --git a/src/components/pages/settings/ConnectCommunities.tsx b/src/components/pages/settings/ConnectCommunities.tsx index 41de9044..19a3f4b3 100644 --- a/src/components/pages/settings/ConnectCommunities.tsx +++ b/src/components/pages/settings/ConnectCommunities.tsx @@ -6,23 +6,24 @@ import CustomButton from '../../global/CustomButton'; import DatePeriodRange from '../../global/DatePeriodRange'; import CustomModal from '../../global/CustomModal'; import ChannelSelection from './ChannelSelection'; -import { BsClockHistory } from 'react-icons/bs'; +import { BsClockHistory, BsTwitter } from 'react-icons/bs'; import useAppStore from '../../../store/useStore'; import { useRouter } from 'next/router'; import moment from 'moment'; import { StorageService } from '../../../services/StorageService'; import { IUser } from '../../../utils/types'; -import * as amplitude from '@amplitude/analytics-browser'; -import { IDecodedToken } from '../../../utils/interfaces'; -import jwt_decode from 'jwt-decode'; + import { setAmplitudeUserIdFromToken, trackAmplitudeEvent, } from '../../../helpers/amplitudeHelper'; +import { decodeUserTokenDiscordId } from '../../../helpers/helper'; export default function ConnectCommunities() { const router = useRouter(); + const user = StorageService.readLocalStorage('user'); + const [open, setOpen] = useState(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); const [guildId, setGuildId] = useState(''); @@ -30,8 +31,13 @@ export default function ConnectCommunities() { const [datePeriod, setDatePeriod] = useState(''); const [selectedChannels, setSelectedChannels] = useState([]); - const { guilds, connectNewGuild, patchGuildById, getUserGuildInfo } = - useAppStore(); + const { + guilds, + connectNewGuild, + patchGuildById, + getUserGuildInfo, + authorizeTwitter, + } = useAppStore(); if (typeof window !== 'undefined') { useEffect(() => { @@ -121,6 +127,15 @@ export default function ConnectCommunities() { getUserGuildInfo(guildId); setConfirmModalOpen(false); }; + + const handleAuthorizeTwitter = () => { + authorizeTwitter(decodeUserTokenDiscordId(user)); + }; + const isAllTwitterPropertiesNull = + user && + user.twitter && + Object.values(user.twitter).every((value) => value == null); + return ( <>
-
+

Connect your communities

- {guilds.length >= 1 ? ( - - It will be possible to connect more communities soon. - - } - arrow - placement="right" - > - -

Discord

- -
- -

Connect

-
-
-
- ) : ( - connectNewGuild()} - > -

Discord

- -
- -

Connect

+
+ {isAllTwitterPropertiesNull ? ( +
+ handleAuthorizeTwitter()} + > +

Twitter

+ +
+ +

Connect

+
+
- - )} + ) : ( + <> + )} +
+ {guilds.length >= 1 ? ( + + It will be possible to connect more communities soon. + + } + arrow + placement="right" + > + +

Discord

+ +
+ +

Connect

+
+
+
+ ) : ( + connectNewGuild()} + > +

Discord

+ +
+ +

Connect

+
+
+ )} +
+
diff --git a/src/components/pages/settings/ConnectedCommunitiesList.tsx b/src/components/pages/settings/ConnectedCommunitiesList.tsx index ab22c9f7..f1972bb2 100644 --- a/src/components/pages/settings/ConnectedCommunitiesList.tsx +++ b/src/components/pages/settings/ConnectedCommunitiesList.tsx @@ -8,12 +8,13 @@ import { Paper } from '@mui/material'; import useAppStore from '../../../store/useStore'; import { DISCONNECT_TYPE } from '../../../store/types/ISetting'; import { StorageService } from '../../../services/StorageService'; -import { IUser } from '../../../utils/types'; +import { ITwitter, IUser } from '../../../utils/types'; import { setAmplitudeUserIdFromToken, trackAmplitudeEvent, } from '../../../helpers/amplitudeHelper'; +import ConnectedTwitter from './ConnectedTwitter'; export default function ConnectedCommunitiesList({ guilds }: any) { const { disconnecGuildById, getGuilds } = useAppStore(); @@ -22,6 +23,7 @@ export default function ConnectedCommunitiesList({ guilds }: any) { const toggleModal = (e: boolean) => { setOpen(e); }; + let user: IUser | undefined = StorageService.readLocalStorage('user'); const notify = () => { toast('The integration has been disconnected succesfully.', { position: 'top-center', @@ -42,9 +44,6 @@ export default function ConnectedCommunitiesList({ guilds }: any) { notify(); getGuilds(); - let user: IUser | undefined = - StorageService.readLocalStorage('user'); - setAmplitudeUserIdFromToken(); trackAmplitudeEvent({ @@ -61,6 +60,15 @@ export default function ConnectedCommunitiesList({ guilds }: any) { }); }; + function isAllTwitterPropertiesNull(twitter: ITwitter): boolean { + return ( + twitter.twitterConnectedAt === null && + twitter.twitterId === null && + twitter.twitterProfileImageUrl === null && + twitter.twitterUsername === null + ); + } + return ( <> {guilds && guilds.length > 0 ? ( @@ -82,6 +90,13 @@ export default function ConnectedCommunitiesList({ guilds }: any) {
)) : ''} + {user?.twitter && !isAllTwitterPropertiesNull(user.twitter) ? ( +
+ +
+ ) : ( + <> + )}
) : ( diff --git a/src/components/pages/settings/ConnectedTwitter.tsx b/src/components/pages/settings/ConnectedTwitter.tsx new file mode 100644 index 00000000..e6968be7 --- /dev/null +++ b/src/components/pages/settings/ConnectedTwitter.tsx @@ -0,0 +1,73 @@ +import { Avatar, Paper } from '@mui/material'; +import React from 'react'; +import { ITwitter } from '../../../utils/types'; +import useAppStore from '../../../store/useStore'; +import { BsTwitter } from 'react-icons/bs'; +import moment from 'moment'; +import { StorageService } from '../../../services/StorageService'; +import clsx from 'clsx'; + +interface IConnectedTwitter { + twitter?: ITwitter; +} + +function ConnectedTwitter({ twitter }: IConnectedTwitter) { + const { disconnectTwitter, getUserInfo } = useAppStore(); + + const handleDisconnect = async () => { + await disconnectTwitter(); + const { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + } = await getUserInfo(); + + StorageService.updateLocalStorageWithObject('user', 'twitter', { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + }); + StorageService.removeLocalStorage('lastTwitterMetricsRefreshDate'); + }; + + return ( +
+ +
+
+

Twitter

+ +
+ +
+
+ +
+

{twitter?.twitterUsername}

+

{`Connected ${moment( + twitter?.twitterConnectedAt + ).format('DD MMM yyyy')}`}

+
+
+
+ Disconnect +
+
+
+ ); +} + +export default ConnectedTwitter; diff --git a/src/components/shared/TcAlert.spec.tsx b/src/components/shared/TcAlert.spec.tsx new file mode 100644 index 00000000..4e83d18d --- /dev/null +++ b/src/components/shared/TcAlert.spec.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcAlert from './TcAlert'; // Replace with the correct import path + +describe('TcAlert Component', () => { + it('renders the alert with custom message', () => { + const message = 'This is a test alert'; + + const { getByText } = render({message}); + + // Use the @testing-library/jest-dom assertions to check if the element is present + expect(getByText(message)).toBeInTheDocument(); + }); + + it('renders the alert with a custom severity', () => { + const message = 'Custom severity alert'; + const customSeverity = 'warning'; + + const { container } = render( + {message} + ); + + // Check if the element has the correct class based on the custom severity + expect(container).toHaveTextContent(message); + }); +}); diff --git a/src/components/shared/TcAlert.tsx b/src/components/shared/TcAlert.tsx new file mode 100644 index 00000000..a54f194d --- /dev/null +++ b/src/components/shared/TcAlert.tsx @@ -0,0 +1,20 @@ +import { Alert, AlertProps } from '@mui/material'; +import React from 'react'; + +/** + * TcAlert Component + * + * A simple wrapper component for MUI's Alert component. It allows you to + * use MUI Alert with the flexibility to pass any valid AlertProps. + * + * @param {ITcAlertProps} props - The props for the TcAlert component. + * @returns {React.ReactElement} A React element representing the Alert component. + */ + +interface ITcAlertProps extends AlertProps {} + +function TcAlert({ ...rest }: ITcAlertProps) { + return ; +} + +export default TcAlert; diff --git a/src/components/shared/TcBox/TcBoxContainer.spec.tsx b/src/components/shared/TcBox/TcBoxContainer.spec.tsx new file mode 100644 index 00000000..8fff8df9 --- /dev/null +++ b/src/components/shared/TcBox/TcBoxContainer.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcBoxContainer from './TcBoxContainer'; + +// Mock the child components +jest.mock('./TcBoxTitleContainer', () => (props: any) => ( +
{props.children}
+)); +jest.mock('./TcBoxContentContainer', () => (props: any) => ( +
{props.children}
+)); + +describe('', () => { + it('renders title and content children correctly', () => { + const titleChild = Title Child; + const contentChild = Content Child; + + const { getByTestId, getByText } = render( + + ); + + const titleContainer = getByTestId('mock-title-container'); + const contentContainer = getByTestId('mock-content-container'); + + expect(titleContainer).toContainElement(getByText('Title Child')); + expect(contentContainer).toContainElement(getByText('Content Child')); + }); +}); diff --git a/src/components/shared/TcBox/TcBoxContainer.tsx b/src/components/shared/TcBox/TcBoxContainer.tsx new file mode 100644 index 00000000..694f61a0 --- /dev/null +++ b/src/components/shared/TcBox/TcBoxContainer.tsx @@ -0,0 +1,40 @@ +/** + * TcBoxContainer Component. + * + * This is a container component that combines a title and content area. + * + * Props: + * - titleContainerChildren: Element that will be rendered in the title container. + * - contentContainerChildren: Element that will be rendered in the content container. + * + * Example: + * ```jsx + * Title} + * contentContainerChildren={

Some content here.

} + * /> + * ``` + */ + +import { Box, BoxProps } from '@mui/material'; +import TcBoxTitleContainer from './TcBoxTitleContainer'; +import TcBoxContentContainer from './TcBoxContentContainer'; + +interface ITcBoxContainer extends Omit { + titleContainerChildren: JSX.Element | React.ReactElement; + contentContainerChildren: JSX.Element | React.ReactElement; +} + +function TcBoxContainer({ + titleContainerChildren, + contentContainerChildren, +}: ITcBoxContainer) { + return ( + + + + + ); +} + +export default TcBoxContainer; diff --git a/src/components/shared/TcBox/TcBoxContentContainer.spec.tsx b/src/components/shared/TcBox/TcBoxContentContainer.spec.tsx new file mode 100644 index 00000000..e1e74bc0 --- /dev/null +++ b/src/components/shared/TcBox/TcBoxContentContainer.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcBoxContentContainer from './TcBoxContentContainer'; + +describe('', () => { + // Test: Renders children correctly + it('renders children correctly', () => { + const { getByText } = render( + Test Content} /> + ); + + expect(getByText('Test Content')).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcBox/TcBoxContentContainer.tsx b/src/components/shared/TcBox/TcBoxContentContainer.tsx new file mode 100644 index 00000000..e30d951d --- /dev/null +++ b/src/components/shared/TcBox/TcBoxContentContainer.tsx @@ -0,0 +1,27 @@ +/** + * TcBoxContentContainer Component. + * + * A generic container for content. + * + * Props: + * - children: The content that will be displayed inside this container. + * + * Example: + * ```jsx + * + *

This is some content inside the container.

+ *
+ * ``` + */ + +import React from 'react'; + +interface ITcBoxContentContainer { + children: JSX.Element | React.ReactElement; +} + +function TcBoxContentContainer({ children }: ITcBoxContentContainer) { + return
{children}
; +} + +export default TcBoxContentContainer; diff --git a/src/components/shared/TcBox/TcBoxTitleContainer.spec.tsx b/src/components/shared/TcBox/TcBoxTitleContainer.spec.tsx new file mode 100644 index 00000000..de089913 --- /dev/null +++ b/src/components/shared/TcBox/TcBoxTitleContainer.spec.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcBoxTitleContainer from './TcBoxTitleContainer'; + +describe('', () => { + // Test 1: Renders children correctly + it('renders children correctly', () => { + const { getByText } = render( + Test Content} /> + ); + + expect(getByText('Test Content')).toBeInTheDocument(); + }); + + // Test 2: Applies custom classes + it('applies custom classes correctly', () => { + const { container } = render( + Test Content} + customClasses="test-class1 test-class2" + /> + ); + + const div = container.firstChild; + expect(div).toHaveClass('test-class1'); + expect(div).toHaveClass('test-class2'); + }); +}); diff --git a/src/components/shared/TcBox/TcBoxTitleContainer.tsx b/src/components/shared/TcBox/TcBoxTitleContainer.tsx new file mode 100644 index 00000000..e25677c5 --- /dev/null +++ b/src/components/shared/TcBox/TcBoxTitleContainer.tsx @@ -0,0 +1,33 @@ +/** + * TcBoxTitleContainer Component. + * + * A container dedicated for displaying titles. + * + * Props: + * - children: The content that needs to be displayed inside this container. + * - customClasses (optional): Additional classnames that can be added to the container for custom styling. + * + * Example: + * ```jsx + * + *

Title Goes Here

+ *
+ * ``` + */ + +import clsx from 'clsx'; +import React from 'react'; + +interface ITcBoxTitleContainer { + children: JSX.Element; + customClasses?: string; +} + +function TcBoxTitleContainer({ + children, + customClasses, +}: ITcBoxTitleContainer) { + return
{children}
; +} + +export default TcBoxTitleContainer; diff --git a/src/components/shared/TcBox/index.ts b/src/components/shared/TcBox/index.ts new file mode 100644 index 00000000..2a244a88 --- /dev/null +++ b/src/components/shared/TcBox/index.ts @@ -0,0 +1,3 @@ +import { default as TcBoxContainer } from './TcBoxContainer'; + +export default { TcBoxContainer }; diff --git a/src/components/shared/TcButton.spec.tsx b/src/components/shared/TcButton.spec.tsx new file mode 100644 index 00000000..39c7a62f --- /dev/null +++ b/src/components/shared/TcButton.spec.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // for the "toBeInTheDocument" matcher +import TcButton from './TcButton'; + +describe('', () => { + test('renders contained button with TcText wrapping', () => { + render(); + const buttonElement = screen.getByText('Sample Text'); + expect(buttonElement).toBeInTheDocument(); + }); + + test('renders outlined button with TcText wrapping', () => { + render(); + const buttonElement = screen.getByText('Sample Text'); + expect(buttonElement).toBeInTheDocument(); + }); + + test('renders button without variant with direct text', () => { + render(); + const buttonElement = screen.getByText('Sample Text'); + expect(buttonElement).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcButton.tsx b/src/components/shared/TcButton.tsx new file mode 100644 index 00000000..77cacca4 --- /dev/null +++ b/src/components/shared/TcButton.tsx @@ -0,0 +1,38 @@ +/** + * `TcButton` functional component. + * + * This component is an enhanced version of Material-UI's Button component. Depending on the `variant` prop, + * it customizes the appearance of the button's content. If the variant is either 'contained' or 'outlined', + * it wraps the text inside the `TcText` component. Otherwise, it simply displays the text. + * + * @param {ITcButtonProps} props - Properties passed to the component. + * @returns {React.ReactElement} Rendered button component. + */ + +import { Button, ButtonProps } from '@mui/material'; +import React from 'react'; +import TcText from './TcText'; + +interface ITcButtonProps extends ButtonProps { + text: string; +} + +function TcButton({ text, ...props }: ITcButtonProps) { + if (props.variant === 'contained') { + return ( + + ); + } + if (props.variant === 'outlined') { + return ( + + ); + } + return ; +} + +export default TcButton; diff --git a/src/components/shared/TcCard.spec.tsx b/src/components/shared/TcCard.spec.tsx new file mode 100644 index 00000000..b1762603 --- /dev/null +++ b/src/components/shared/TcCard.spec.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcCard from './TcCard'; +import { Typography, CardContent } from '@mui/material'; + +describe('', () => { + // Test 1: Check if the component renders correctly + it('renders without crashing', () => { + render(test} />); + }); + + // Test 2: Check if the component renders its children + it('renders its children', () => { + const { getByText } = render( + + + Card Content + + + ); + + expect(getByText('Card Content')).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcCard.tsx b/src/components/shared/TcCard.tsx new file mode 100644 index 00000000..0a7f1f80 --- /dev/null +++ b/src/components/shared/TcCard.tsx @@ -0,0 +1,33 @@ +/** + * TcCard Component. + * + * A wrapper around the Material-UI Card component with the added constraint that it must contain children. + * This component extends all properties of the MUI Card, making it versatile for various use cases. + * + * Props: + * - children: The content to be displayed inside the card. It's a mandatory prop for TcCard. + * - ...rest: All other properties supported by the MUI Card component. Refer to MUI documentation for a complete list. + * + * Example: + * ```jsx + * + * + * Card Title + * Card content goes here. + * + * + * ``` + */ + +import { Card, CardProps } from '@mui/material'; +import React from 'react'; + +interface ITcCard extends CardProps { + children: JSX.Element | React.ReactElement; +} + +function TcCard({ children, ...rest }: ITcCard) { + return {children}; +} + +export default TcCard; diff --git a/src/components/shared/TcCheckbox.spec.tsx b/src/components/shared/TcCheckbox.spec.tsx new file mode 100644 index 00000000..d57d6d30 --- /dev/null +++ b/src/components/shared/TcCheckbox.spec.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // For the "toBeInTheDocument" matcher and other extended matchers + +import TcCheckbox from './TcCheckbox'; + +describe('TcCheckbox', () => { + it('renders without crashing', () => { + render(); + }); +}); diff --git a/src/components/shared/TcCheckbox.tsx b/src/components/shared/TcCheckbox.tsx new file mode 100644 index 00000000..88f26005 --- /dev/null +++ b/src/components/shared/TcCheckbox.tsx @@ -0,0 +1,39 @@ +/** + * `TcCheckbox` Component + * + * This component is a simple wrapper around MUI's Checkbox component. + * It provides an encapsulated way to manage and use checkboxes, while + * offering all the extended properties and behaviors of the underlying MUI Checkbox. + * + * Props: + * All properties of the original MUI Checkbox component are supported. Refer to + * MUI documentation for detailed prop types and descriptions. + * @see https://mui.com/api/checkbox/ + * + * Usage: + * ```jsx + * + * ``` + * + * Note: + * - Ensure that you manage the checked state externally when using this component. + * - Bind an onChange handler to capture and manage checkbox state changes. + * + * @param {ITcCheckboxProps} props - Extended MUI Checkbox properties. + * @returns {JSX.Element} Rendered Checkbox component. + */ + +import React from 'react'; +import Checkbox, { CheckboxProps } from '@mui/material/Checkbox'; + +interface ITcCheckboxProps extends CheckboxProps {} + +function TcCheckbox({ ...props }: ITcCheckboxProps) { + return ; +} + +export default TcCheckbox; diff --git a/src/components/shared/TcCollapse.spec.tsx b/src/components/shared/TcCollapse.spec.tsx new file mode 100644 index 00000000..1444bd10 --- /dev/null +++ b/src/components/shared/TcCollapse.spec.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcCollapse from './TcCollapse'; // Replace with the correct import path + +describe('TcCollapse Component', () => { + it('renders children content when open', () => { + const { getByText } = render( + +
Content inside Collapse
+
+ ); + + const content = getByText('Content inside Collapse'); + + // Check if the content is visible when open + expect(content).toBeVisible(); + }); +}); diff --git a/src/components/shared/TcCollapse.tsx b/src/components/shared/TcCollapse.tsx new file mode 100644 index 00000000..f3e24d74 --- /dev/null +++ b/src/components/shared/TcCollapse.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Collapse, { CollapseProps } from '@mui/material/Collapse'; + +/** + * Custom wrapper component for MUI's Collapse component. + * + * @param {ITcCollapseProps} props - The props for the TcCollapse component. + * @returns {React.ReactElement} A React element representing the Collapse component. + */ + +/** + * Interface for the props of the TcCollapse component, extending CollapseProps. + * + * @interface + */ +interface ITcCollapseProps extends CollapseProps { + /** + * The children prop represents the content to be displayed inside the Collapse component. + * + * @type {React.ReactElement | JSX.Element} + */ + children: React.ReactElement | JSX.Element; +} + +function TcCollapse({ children, ...rest }: ITcCollapseProps) { + return {children}; +} + +export default TcCollapse; diff --git a/src/components/shared/TcDialog/TcDialog.spec.tsx b/src/components/shared/TcDialog/TcDialog.spec.tsx new file mode 100644 index 00000000..d432f10a --- /dev/null +++ b/src/components/shared/TcDialog/TcDialog.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcDialog from './TcDialog'; + +describe('TcDialog Component', () => { + it('renders the dialog with children content', () => { + const { getByText } = render( + +
Dialog Content
+
+ ); + + const content = getByText('Dialog Content'); + expect(content).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcDialog/TcDialog.tsx b/src/components/shared/TcDialog/TcDialog.tsx new file mode 100644 index 00000000..b3eb450f --- /dev/null +++ b/src/components/shared/TcDialog/TcDialog.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Dialog, DialogProps } from '@mui/material'; + +/** + * `TcDialog` Component + * + * A custom wrapper component for Material-UI's Dialog component, allowing you + * to easily control the visibility of the dialog using the `open` prop. + * + * @param {ITcDialogProps} props - Extended Dialog properties. + * @returns {React.ReactElement} A React element representing the Dialog component. + */ +interface ITcDialogProps extends DialogProps { + /** + * The children prop represents the content to be displayed inside the Dialog component. + * + * @type {React.ReactNode } + */ + children: React.ReactNode; +} + +function TcDialog({ children, ...rest }: ITcDialogProps) { + return {children}; +} + +export default TcDialog; diff --git a/src/components/shared/TcDialog/index.ts b/src/components/shared/TcDialog/index.ts new file mode 100644 index 00000000..474d824d --- /dev/null +++ b/src/components/shared/TcDialog/index.ts @@ -0,0 +1,3 @@ +import { default as TcDialog } from './TcDialog'; + +export default TcDialog; diff --git a/src/components/shared/TcIconWithTooltip.spec.tsx b/src/components/shared/TcIconWithTooltip.spec.tsx new file mode 100644 index 00000000..cf0f9705 --- /dev/null +++ b/src/components/shared/TcIconWithTooltip.spec.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcIconWithTooltip from './TcIconWithTooltip'; + +describe('', () => { + // Test 1: Check if the component renders correctly + it('renders without crashing', () => { + render(); + }); + + // Test 2: Check if the default icon (SVG) is rendered if none is provided + it('renders the default SVG icon if none is provided', () => { + render(); + const svgIcon = screen.getByTestId('icon-svg'); // Make sure you add this test-id to your SVG in the component + expect(svgIcon).toBeInTheDocument(); + }); + + // Test 3: Check if a custom icon is rendered when provided + it('renders a custom icon (SVG) when provided', () => { + const customIcon = ; + render( + + ); + const renderedIcon = screen.getByTestId('custom-icon'); + expect(renderedIcon).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcIconWithTooltip.tsx b/src/components/shared/TcIconWithTooltip.tsx new file mode 100644 index 00000000..be6d2e27 --- /dev/null +++ b/src/components/shared/TcIconWithTooltip.tsx @@ -0,0 +1,41 @@ +/** + * TcIconWithTooltip Component. + * + * This component displays an icon wrapped with a Material-UI Tooltip. When the icon is hovered over, + * a tooltip is displayed above the icon with the provided description. + * + * Props: + * - iconComponent: The icon that will be displayed. By default, it uses `MdOutlineInfo` from `react-icons/md`. + * - tooltipText: The text content that will be displayed inside the tooltip. + * + * Example: + * ```jsx + * + * } tooltipText="This is a help icon" /> + * ``` + */ + +import { Tooltip } from '@mui/material'; +import React from 'react'; +import { MdOutlineInfo } from 'react-icons/md'; + +interface ITcIconWithTooltip { + iconComponent?: React.ReactElement | JSX.Element; + tooltipText: string; +} + +function TcIconWithTooltip({ iconComponent, tooltipText }: ITcIconWithTooltip) { + return ( + +
{iconComponent}
+
+ ); +} + +TcIconWithTooltip.defaultProps = { + iconComponent: ( + + ), +}; + +export default TcIconWithTooltip; diff --git a/src/components/shared/TcLink.spec.tsx b/src/components/shared/TcLink.spec.tsx new file mode 100644 index 00000000..80da82ef --- /dev/null +++ b/src/components/shared/TcLink.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcLink from './TcLink'; + +describe('TcLink component', () => { + test('Render component correctly', () => { + const defaultText = 'Title Test'; + + //arrange + const { getByText } = render({defaultText}); + + //assert + expect(getByText(defaultText)).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcLink.tsx b/src/components/shared/TcLink.tsx new file mode 100644 index 00000000..24584b0a --- /dev/null +++ b/src/components/shared/TcLink.tsx @@ -0,0 +1,36 @@ +/** + * TcLink Component + * + * This component serves as a wrapper around the MUI's Link component with custom properties. + * It's designed to provide a standardized link appearance and behavior throughout the application. + * + * @component + * + * @param {object} props - The properties object. + * @param {ReactNode} props.children - The content of the link (e.g., text, icons). + * @param {string} props.to - The URL that the link should point to. + * @param {...MuiLinkProps} rest - The rest of the properties that can be passed to the MUI Link component. + * + * @returns {ReactElement} The TcLink component. + * + * @example + * // Usage example: + * Visit Example + */ + +import React from 'react'; +import { Link, LinkProps as MuiLinkProps } from '@mui/material'; + +interface CustomLinkProps extends MuiLinkProps { + to: string; +} + +function TcLink({ children, to, ...rest }: CustomLinkProps) { + return ( + + {children} + + ); +} + +export default TcLink; diff --git a/src/components/shared/TcText.spec.tsx b/src/components/shared/TcText.spec.tsx new file mode 100644 index 00000000..b5b45d20 --- /dev/null +++ b/src/components/shared/TcText.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcTitle from './TcText'; + +describe('TcTitle component', () => { + test('Render component correctly', () => { + const defaultText = 'Title Test'; + //arrange + const { getByText } = render(); + //act + + //assert + expect(getByText(defaultText)).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcText.tsx b/src/components/shared/TcText.tsx new file mode 100644 index 00000000..b238ec0f --- /dev/null +++ b/src/components/shared/TcText.tsx @@ -0,0 +1,62 @@ +import { Typography, TypographyProps } from '@mui/material'; +import React from 'react'; + +/** + * TcTitle Component + * + * Description: + * The `TcTitle` component is a wrapper around MUI's `Typography` component + * with specific restrictions on the typography variants that can be utilized. + * + * Props: + * - `title`: (Required) A string that represents the text content to be rendered. + * - `variant`: (Required) A string that determines the typography variant + * to use for styling the text content. The allowed variants are: + * - h3 + * - h4 + * - h6 + * - subtitle1 + * - subtitle2 + * - body1 + * - body2 + * - button + * - caption + * + * All other props available to MUI's `Typography` component can also be + * passed to `TcTitle` except for the `variant` prop which is strictly typed + * to the above variants. + * + * Usage: + * ```jsx + * + * ``` + * + * Note: + * This component is specifically designed to restrict the use of typography + * variants to maintain a consistent font-size throughout our application. + * Make sure to only use the allowed variants listed above. Any other variant + * from MUI's `Typography` component is not permitted with `TcTitle`. + * + */ + +type AcceptedVariants = + | 'h3' + | 'h4' + | 'h6' + | 'subtitle1' + | 'subtitle2' + | 'body1' + | 'body2' + | 'button' + | 'caption'; + +interface ITcTextProps extends Omit { + text: string | number | React.ReactNode | JSX.Element; + variant: AcceptedVariants; +} + +function TcText({ text, ...rest }: ITcTextProps) { + return {text}; +} + +export default TcText; diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivity.spec.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivity.spec.tsx new file mode 100644 index 00000000..afabbc4c --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivity.spec.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import TcAccountActivity from './TcAccountActivity'; +import TcAccountActivityHeader from './TcAccountActivityHeader'; +import TcAccountActivityContent from './TcAccountActivityContent'; +import { unmountComponentAtNode } from 'react-dom'; + +// Mocking the child components to check only if they're rendered +jest.mock('./TcAccountActivityHeader', () => { + return { + __esModule: true, + default: jest.fn(() =>
), + }; +}); + +jest.mock('./TcAccountActivityContent', () => { + return { + __esModule: true, + default: jest.fn(() =>
), + }; +}); + +describe('', () => { + let container: any = null; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container.remove(); + container = null; + }); + + it('renders without crashing', () => { + render(, container); + }); + + it('renders TcAccountActivityHeader component', () => { + const { getByTestId } = render(, container); + expect(getByTestId('header-mock')).toBeInTheDocument(); + }); + + it('renders TcAccountActivityContent component', () => { + const { getByTestId } = render(, container); + expect(getByTestId('content-mock')).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivity.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivity.tsx new file mode 100644 index 00000000..f69bc602 --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivity.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react'; +import TcAccountActivityHeader from './TcAccountActivityHeader'; +import TcAccountActivityContent from './TcAccountActivityContent'; +import { IAccount } from '../../../../utils/interfaces'; + +interface ITcAccountActivityProps { + account: IAccount; +} + +interface IAccountItems { + description: string; + value: number; + hasTooltipInfo: boolean; +} + +function TcAccountActivity({ account }: ITcAccountActivityProps) { + const [activityAccount, setActivityAccount] = useState([ + { + description: 'Accounts that engage with you', + value: 0, + hasTooltipInfo: true, + }, + { + description: 'Your followers', + value: 0, + hasTooltipInfo: false, + }, + ]); + + useEffect(() => { + if (account) { + const updatedAccountActivity = [ + { + description: 'Accounts that engage with you', + value: account.engagement, + hasTooltipInfo: true, + }, + { + description: 'Your followers', + value: account.follower, + hasTooltipInfo: false, + }, + ]; + + setActivityAccount(updatedAccountActivity); + } + }, [account]); + + return ( + <> + + + + ); +} + +export default TcAccountActivity; diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivityContent.spec.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivityContent.spec.tsx new file mode 100644 index 00000000..58d802da --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivityContent.spec.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcAccountActivityContent from './TcAccountActivityContent'; + +describe('', () => { + // Test 1: Check if the component renders correctly + it('renders without crashing', () => { + render( + + ); + }); + + it('renders the correct data in cards', () => { + render( + + ); + + expect(screen.getByText('10')).toBeInTheDocument(); + expect( + screen.getByText('Accounts that engage with you') + ).toBeInTheDocument(); + expect(screen.getByText('20')).toBeInTheDocument(); + expect(screen.getByText('Your followers')).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivityContent.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivityContent.tsx new file mode 100644 index 00000000..bd9501ac --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivityContent.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import TcCard from '../../../shared/TcCard'; +import TcText from '../../../shared/TcText'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; + +interface ITcAccountActivityContentProps { + activityList: { + description: string; + value: number; + hasTooltipInfo: boolean; + }[]; +} + +function TcAccountActivityContent({ + activityList, +}: ITcAccountActivityContentProps) { + return ( +
+ {activityList && + activityList.map((el, index) => ( + + + +
+ {el.hasTooltipInfo ? ( + + ) : ( + '' + )} +
+
+ } + /> + ))} +
+ ); +} + +export default TcAccountActivityContent; diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.spec.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.spec.tsx new file mode 100644 index 00000000..f2258748 --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StorageService } from '../../../../services/StorageService'; +import TcAccountActivityHeader from './TcAccountActivityHeader'; + +jest.mock('../../../../services/StorageService'); + +describe('', () => { + beforeEach(() => { + // Mocking the `readLocalStorage` method + const mockedReadLocalStorage = + StorageService.readLocalStorage as jest.MockedFunction< + typeof StorageService.readLocalStorage + >; + + mockedReadLocalStorage.mockReturnValue({ + twitter: { + twitterUsername: 'testUser', + }, + }); + + render(); + }); + + it('renders the main header text', () => { + const headerText = screen.getByText('Account activity'); + expect(headerText).toBeInTheDocument(); + }); + + it('renders the time information with an icon', () => { + const timeInfoText = screen.getByText('Data over the last 7 days'); + const timeIcon = screen.getByTestId('bi-time-five-icon'); + expect(timeInfoText).toBeInTheDocument(); + expect(timeIcon).toBeInTheDocument(); + }); + + it('renders the analyzed account username when provided', () => { + const usernameLink = screen.getByText('@testUser'); + expect(usernameLink).toBeInTheDocument(); + expect(usernameLink).toHaveAttribute('href', '/settings'); + }); +}); diff --git a/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.tsx b/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.tsx new file mode 100644 index 00000000..8fa14290 --- /dev/null +++ b/src/components/twitter/growth/accountActivity/TcAccountActivityHeader.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { BiTimeFive } from 'react-icons/bi'; +import TcText from '../../../shared/TcText'; +import TcLink from '../../../shared/TcLink'; +import { StorageService } from '../../../../services/StorageService'; +import { IUser } from '../../../../utils/types'; + +export default function TcAccountActivityHeader() { + const user = StorageService.readLocalStorage('user'); + return ( +
+
+ +
+ + +
+
+
+ + {user?.twitter?.twitterUsername ? ( + <> + + @{user?.twitter?.twitterUsername} + + + ) : ( + + )} +
+
+ ); +} diff --git a/src/components/twitter/growth/accountActivity/index.ts b/src/components/twitter/growth/accountActivity/index.ts new file mode 100644 index 00000000..1390b1f9 --- /dev/null +++ b/src/components/twitter/growth/accountActivity/index.ts @@ -0,0 +1 @@ +import { default as TcAccountActivity } from './TcAccountActivity'; diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponse.spec.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponse.spec.tsx new file mode 100644 index 00000000..72349c42 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponse.spec.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcAudienceResponse from './TcAudienceResponse'; + +describe('', () => { + const mockAudience = { + replies: 50, + retweets: 30, + mentions: 20, + posts: 0, + likes: 0, + }; + + beforeEach(() => { + render(); + }); + + // Test 1: Check if the "Audience response" header text from `TcAudienceResponseHeader` is rendered + it('renders the header text correctly', () => { + const headerText = screen.getByText('Audience response'); + expect(headerText).toBeInTheDocument(); + }); + + // Test 2: Check if any one of the descriptions from `TcAudienceResponseContent` is rendered + it('renders the content descriptions correctly based on provided audience data', () => { + // Using the data in mockAudience for validation + const repliesDescription = screen.getByText('Replies'); + const retweetsDescription = screen.getByText('Retweets'); + const mentionsDescription = screen.getByText('Mentions'); + + expect(repliesDescription).toBeInTheDocument(); + expect(retweetsDescription).toBeInTheDocument(); + expect(mentionsDescription).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponse.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponse.tsx new file mode 100644 index 00000000..e8b62a31 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponse.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from 'react'; +import TcAudienceResponseHeader from './TcAudienceResponseHeader'; +import TcAudienceResponseContent from './TcAudienceResponseContent'; +import { capitalizeFirstChar } from '../../../../helpers/helper'; +import { IAudience } from '../../../../utils/interfaces'; +interface ITcAudienceResponseProps { + audience: IAudience; +} + +interface IAccountAudienceItem { + description: string; + value: number; + hasTooltipInfo: boolean; +} + +function TcAudienceResponse({ audience }: ITcAudienceResponseProps) { + const [audienceResponseList, setAudienceResponseList] = useState< + IAccountAudienceItem[] + >([]); + + useEffect(() => { + if (audience) { + const newState = Object.keys(audience).map((key) => { + const audienceKey = key as keyof IAudience; + return { + description: capitalizeFirstChar(audienceKey), + value: audience[audienceKey], + hasTooltipInfo: false, + }; + }); + setAudienceResponseList(newState); + } + }, [audience]); + + return ( +
+ + +
+ ); +} + +export default TcAudienceResponse; diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.spec.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.spec.tsx new file mode 100644 index 00000000..e96d5057 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.spec.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcAudienceResponseContent from './TcAudienceResponseContent'; + +describe('', () => { + const mockData = [ + { description: 'Replies', value: 50, hasTooltipInfo: false }, + { description: 'Retweets', value: 30, hasTooltipInfo: false }, + { description: 'Likes', value: 25, hasTooltipInfo: false }, + { description: 'Mentions', value: 20, hasTooltipInfo: false }, + ]; + + beforeEach(() => { + render(); + }); + + // Test 1: Check if the correct number of cards are rendered + it('renders the correct number of cards', () => { + const cards = screen.getAllByText(/Replies|Retweets|Likes|Mentions/); + expect(cards.length).toBe(4); + }); + + // Test 2: Check if the TcIconWithTooltip is not present for all cards + it('does not render any tooltips', () => { + const tooltipText = 'Followers and non-followers'; + expect(screen.queryByText(tooltipText)).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.tsx new file mode 100644 index 00000000..99874e14 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponseContent.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import TcCard from '../../../shared/TcCard'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; +import TcText from '../../../shared/TcText'; + +interface IAccountAudienceItem { + description: string; + value: number; + hasTooltipInfo: boolean; +} + +interface ITcAudienceResponseContentProps { + data: IAccountAudienceItem[]; +} + +function TcAudienceResponseContent({ data }: ITcAudienceResponseContentProps) { + return ( +
+
+ {data && + data.map((el, index) => ( + + + +
+ {el.hasTooltipInfo ? ( + + ) : ( + '' + )} +
+
+ } + /> + ))} +
+
+ ); +} + +export default TcAudienceResponseContent; diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.spec.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.spec.tsx new file mode 100644 index 00000000..fb051ba3 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcAudienceResponseHeader from './TcAudienceResponseHeader'; + +describe('', () => { + beforeEach(() => { + render(); + }); + + // Test 1: Check if the header text "Audience response" is rendered + it('renders the header text correctly', () => { + const headerText = screen.getByText('Audience response'); + expect(headerText).toBeInTheDocument(); + }); + + // Test 2: Check if the caption text "How much others react to your activities" is rendered + it('renders the caption text correctly', () => { + const captionText = screen.getByText( + 'How much others react to your activities' + ); + expect(captionText).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.tsx b/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.tsx new file mode 100644 index 00000000..ca777dc4 --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/TcAudienceResponseHeader.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import TcText from '../../../shared/TcText'; + +function TcAudienceResponseHeader() { + return ( +
+ + +
+ ); +} + +export default TcAudienceResponseHeader; diff --git a/src/components/twitter/growth/audienceResponse/index.ts b/src/components/twitter/growth/audienceResponse/index.ts new file mode 100644 index 00000000..8caef6bd --- /dev/null +++ b/src/components/twitter/growth/audienceResponse/index.ts @@ -0,0 +1 @@ +import { default as TcYourAccountActivity } from './TcAudienceResponse'; diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.spec.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.spec.tsx new file mode 100644 index 00000000..0cc1eb9a --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import TcEngagementAccountContentItems from './TcEngagementAccountContentItems'; + +describe('TcEngagementAccountContentItems', () => { + const defaultProps = { + bgColor: 'bg-[#3A9E2B]', + value: '5', + description: 'Sample description', + tooltipText: 'Sample tooltip text', + }; + + it('renders the component and checks presence of value and description', () => { + render(); + + expect(screen.getByText(defaultProps.value.toString())).toBeInTheDocument(); + expect(screen.getByText(defaultProps.description)).toBeInTheDocument(); + }); + + it('renders the tooltip icon when tooltipText prop is provided', () => { + render(); + + expect(screen.getByTestId('icon-svg')).toBeInTheDocument(); + }); + + it('does not render the tooltip icon when tooltipText prop is not provided', () => { + const propsWithoutTooltip = { + ...defaultProps, + tooltipText: undefined, + }; + render(); + + expect(screen.queryByTestId('icon-svg')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.tsx new file mode 100644 index 00000000..12de32e6 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountContentItems.tsx @@ -0,0 +1,57 @@ +import clsx from 'clsx'; +import React from 'react'; +import TcText from '../../../shared/TcText'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; +import { MdOutlineInfo } from 'react-icons/md'; + +interface ITcEngagementAccountContentItemsProps { + value: string | number; + description: string; + tooltipText?: string; + bgColor?: string; +} + +function TcEngagementAccountContentItems({ + bgColor, + value, + description, + tooltipText, +}: ITcEngagementAccountContentItemsProps) { + return ( +
+ + +
+ {tooltipText && ( + + } + /> + )} +
+
+ ); +} + +export default TcEngagementAccountContentItems; diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.spec.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.spec.tsx new file mode 100644 index 00000000..a1e736e3 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcEngagementAccounts from './TcEngagementAccounts'; +import TcEngagementAccountsHeader from './TcEngagementAccountsHeader'; + +describe('', () => { + const mockEngagement = { + hqla: 10, + hqhe: 20, + lqla: 15, + lqhe: 25, + }; + + beforeEach(() => { + render(); + }); + + // Test 1: Check if the TcEngagementAccountsHeader component is rendered. + it('renders the header component', () => { + render(); + }); + + // Test 2: Check if the data is rendered correctly in the TcEngagementAccountsContent component. + it('renders the correct engagement values and descriptions', () => { + const descriptions = [ + 'Only engaged a bit but deeper interactions', + 'Frequently engaged and deep interactions', + 'Only engaged a bit and shallow interactions', + 'Frequently engaged but shallow interactions', + ]; + + descriptions.forEach((description) => { + expect(screen.getByText(description)).toBeInTheDocument(); + }); + + expect( + screen.getByText(mockEngagement.hqla.toString()) + ).toBeInTheDocument(); + expect( + screen.getByText(mockEngagement.hqhe.toString()) + ).toBeInTheDocument(); + expect( + screen.getByText(mockEngagement.lqla.toString()) + ).toBeInTheDocument(); + expect( + screen.getByText(mockEngagement.lqhe.toString()) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.tsx new file mode 100644 index 00000000..3685055a --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccounts.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react'; +import TcEngagementAccountsHeader from './TcEngagementAccountsHeader'; +import TcEngagementAccountsContent from './TcEngagementAccountsContent'; +import { IEngagement } from '../../../../utils/interfaces'; + +interface ITcEngagementAccountsProps { + engagement: IEngagement; +} + +function TcEngagementAccounts({ engagement }: ITcEngagementAccountsProps) { + const [contentItems, setContentItems] = useState([ + { + bgColor: 'bg-[#D2F4CF]', + value: 0, + description: 'Only engaged a bit but deeper interactions', + tooltipText: + 'Number of users with low engagement (less than 3 interactions) but of high quality ( replying, mentioning, or quoting you)', + label: 'Low', + }, + { + bgColor: 'bg-[#3A9E2B]', + value: 0, + description: 'Frequently engaged and deep interactions', + tooltipText: + 'Number of users with high engagement (at least 3) and high quality (replies, quotes, or mentions you)', + }, + { + bgColor: 'bg-[#FBE8DA]', + value: 0, + description: 'Only engaged a bit and shallow interactions', + tooltipText: + 'Number of users with low engagement (less than 3 interactions) but of high quality ( replying, mentioning, or quoting you)', + label: 'Low', + }, + { + bgColor: 'bg-[#D2F4CF]', + value: 0, + description: 'Frequently engaged but shallow interactions', + tooltipText: + 'Number of users with high engagement (at least 3) but low quality (likes and retweets)', + label: 'High', + }, + ]); + + useEffect(() => { + if (engagement) { + const updatedContentItems = [...contentItems]; + + updatedContentItems[0].value = engagement.hqla; + updatedContentItems[1].value = engagement.hqhe; + updatedContentItems[2].value = engagement.lqla; + updatedContentItems[3].value = engagement.lqhe; + + setContentItems(updatedContentItems); + } + }, [engagement]); + + return ( +
+ + +
+ ); +} + +export default TcEngagementAccounts; diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.spec.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.spec.tsx new file mode 100644 index 00000000..5bc50d29 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.spec.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcEngagementAccountsContent from './TcEngagementAccountsContent'; + +describe('', () => { + const mockContentItems = [ + { + bgColor: 'bg-red', + value: 10, + description: 'Description 1', + tooltipText: 'Tooltip 1', + label: 'Label 1', + }, + { + bgColor: 'bg-blue', + value: 20, + description: 'Description 2', + tooltipText: 'Tooltip 2', + }, + { + bgColor: 'bg-yellow', + value: 30, + description: 'Description 3', + tooltipText: 'Tooltip 3', + label: 'Label 3', + }, + { + bgColor: 'bg-green', + value: 40, + description: 'Description 4', + tooltipText: 'Tooltip 4', + }, + ]; + + beforeEach(() => { + render(); + }); + + // Test 1: Check if the TcEngagementAccountContentItems component is rendered for each item. + it('renders content items correctly', () => { + mockContentItems.forEach((item) => { + expect(screen.getByText(item.description)).toBeInTheDocument(); + }); + }); + + // Test 2: Check if the labels 'High' and 'Low' are rendered. + it('renders the labels High and Low', () => { + ['High', 'Low'].forEach((label) => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + }); + + // Test 3: Check if specific labels in content items are rendered. + it('renders content item labels correctly', () => { + mockContentItems.forEach((item) => { + if (item.label) { + expect(screen.getByText(item.label)).toBeInTheDocument(); + } + }); + }); +}); diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.tsx new file mode 100644 index 00000000..ace180f2 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsContent.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import TcText from '../../../shared/TcText'; +import TcEngagementAccountContentItems from './TcEngagementAccountContentItems'; + +interface IContentItem { + bgColor: string; + value: number; + description: string; + tooltipText: string; + label?: string; +} + +interface ITcEngagementAccountsContentProps { + contentItems: IContentItem[]; +} + +function TcEngagementAccountsContent({ + contentItems, +}: ITcEngagementAccountsContentProps) { + const renderContentItems = (item: IContentItem, index: number) => ( +
+ + {item.label && ( + + )} +
+ ); + + return ( +
+
+ +
+
+ +
+
+ {['High', 'Low'].map((label, index) => ( +
+ +
+ {renderContentItems(contentItems[index * 2], index * 2)} + {renderContentItems(contentItems[index * 2 + 1], index * 2 + 1)} +
+
+ ))} +
+
+ ); +} + +export default TcEngagementAccountsContent; diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.spec.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.spec.tsx new file mode 100644 index 00000000..9aece62e --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // For the "toBeInTheDocument" matcher + +import TcAudienceResponseHeader from './TcEngagementAccountsHeader'; + +describe('TcAudienceResponseHeader', () => { + it('renders the correct text', () => { + const { getByText } = render(); + + const headerText = getByText('Engagement by accounts'); + + expect(headerText).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.tsx b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.tsx new file mode 100644 index 00000000..db6f4477 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/TcEngagementAccountsHeader.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import TcText from '../../../shared/TcText'; + +function TcEngagementAccountsHeader() { + return ( + <> + + + ); +} + +export default TcEngagementAccountsHeader; diff --git a/src/components/twitter/growth/engagementAccounts/index.ts b/src/components/twitter/growth/engagementAccounts/index.ts new file mode 100644 index 00000000..dbc6e621 --- /dev/null +++ b/src/components/twitter/growth/engagementAccounts/index.ts @@ -0,0 +1 @@ +import { default as TcEngagementAccounts } from './TcEngagementAccounts'; diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeature.spec.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeature.spec.tsx new file mode 100644 index 00000000..f15f09c5 --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeature.spec.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import TcvoteFeature from './TcvoteFeature'; + +// Mocking child components +jest.mock('./TcvoteFeatureHeader', () => { + return () =>
TcvoteFeatureHeader Mock
; +}); + +jest.mock('./TcvoteFeatureVotes/TcvoteFeatureVotes', () => { + return ({ handleSelectedFeatures }: any) => ( + + ); +}); + +describe('', () => { + test('renders without crashing', () => { + render(); + expect(screen.getByText('TcvoteFeatureHeader Mock')).toBeInTheDocument(); + expect(screen.getByText('Mock TcvoteFeatureVotes')).toBeInTheDocument(); + expect(screen.getByText('Vote now')).toBeInTheDocument(); + }); + + test('"Vote now" button is initially disabled', async () => { + render(); + const button = await waitFor(() => + screen.getByRole('button', { name: /Vote now/i }) + ); + expect(button).toBeDisabled(); + }); + test('"Vote now" button is enabled when features are selected', () => { + render(); + fireEvent.click(screen.getByText('Mock TcvoteFeatureVotes')); // Simulate selecting features + expect(screen.getByText('Vote now')).not.toBeDisabled(); + }); +}); diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeature.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeature.tsx new file mode 100644 index 00000000..2187feae --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeature.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import TcvoteFeatureHeader from './TcvoteFeatureHeader'; +import TcvoteFeatureVotes from './TcvoteFeatureVotes/TcvoteFeatureVotes'; +import TcButton from '../../../shared/TcButton'; + +function TcvoteFeature() { + const [nextFeature, setNextFeature] = useState>([ + false, + false, + false, + false, + ]); + + const handleSelectedFeatures = (selectedFeatures: boolean[]) => { + setNextFeature(selectedFeatures); + }; + + return ( +
+ + +
+ +
+
+ ); +} + +export default TcvoteFeature; diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.spec.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.spec.tsx new file mode 100644 index 00000000..a60accb2 --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // For the "toBeInTheDocument" matcher + +import TcvoteFeatureHeader from './TcvoteFeatureHeader'; + +describe('TcvoteFeatureHeader', () => { + it('renders the TcvoteFeatureHeader text', () => { + const { getByText } = render(); + + const headerText = getByText('Vote on our next feature'); + + expect(headerText).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.tsx new file mode 100644 index 00000000..7709dbfe --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureHeader.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import TcText from '../../../shared/TcText'; + +function TcAudienceResponseHeader() { + return ( +
+ +
+ ); +} + +export default TcAudienceResponseHeader; diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.spec.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.spec.tsx new file mode 100644 index 00000000..89c15eca --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, fireEvent, screen, cleanup } from '@testing-library/react'; +import TcvoteFeatureVotes from './TcvoteFeatureVotes'; + +const TcvoteFeatureVotesMockList = [ + { + label: + 'Member Breakdown: see the accounts in each category to engage with them', + value: 0, + }, + { + label: 'Tweet Scheduling: create tweets directly from the dashboard ', + value: 1, + }, + { + label: + 'Cross Platform Integration: see how many active twitter followers are also active in your discord', + value: 2, + }, + { + label: + 'Targeting: Discover new twitter profiles similar to your most active followers', + value: 3, + }, +]; + +describe('', () => { + test('renders the list items correctly', () => { + for (const item of TcvoteFeatureVotesMockList) { + const mockFn = jest.fn(); + render(); + const textElement = screen.queryByText(item.label); + if (!textElement) { + console.error(`Failed to find: ${item.label}`); + } + cleanup(); + } + }); + + test('updates the state correctly when a checkbox is clicked', () => { + const mockFn = jest.fn(); + render(); + + const firstCheckbox = screen.getAllByRole('checkbox')[0]; + fireEvent.click(firstCheckbox); + + expect(mockFn).toHaveBeenCalledWith([true, false, false, false]); + }); +}); diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.tsx new file mode 100644 index 00000000..cc065682 --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotes.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from 'react'; +import TcvoteFeatureVotesItems from './TcvoteFeatureVotesItems'; + +const TcvoteFeatureVotesMockList = [ + { + label: + 'Member Breakdown: see the accounts in each category to engage with them', + value: 0, + }, + { + label: 'Tweet Scheduling: create tweets directly from the dashboard ', + value: 1, + }, + { + label: + 'Cross Platform Integration: see how many active twitter followers are also active in your discord', + value: 2, + }, + { + label: + 'Targeting: Discover new twitter profiles similar to your most active followers', + value: 3, + }, +]; + +interface ITcvoteFeatureVotesProps { + handleSelectedFeatures: (selectedFeatures: boolean[]) => void; +} + +function TcvoteFeatureVotes({ + handleSelectedFeatures, +}: ITcvoteFeatureVotesProps) { + const [nextFeatures, setNextFeatures] = useState>([ + false, + false, + false, + false, + ]); + + useEffect(() => { + handleSelectedFeatures(nextFeatures); + }, [nextFeatures]); + + const handleToggleCheckbox = (e: boolean, index: number) => { + setNextFeatures((prevState) => + prevState.map((item, idx) => (idx === index ? e : item)) + ); + }; + return ( +
+ {TcvoteFeatureVotesMockList.map((item, index) => ( + handleToggleCheckbox(event, index)} + color="secondary" + /> + ))} +
+ ); +} + +export default TcvoteFeatureVotes; diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.spec.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.spec.tsx new file mode 100644 index 00000000..372e1069 --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.spec.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import TcvoteFeatureVotesItems from './TcvoteFeatureVotesItems'; + +describe('', () => { + test('renders correctly', () => { + render( + + ); + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + test('renders checked checkbox based on isChecked prop', () => { + const { rerender } = render( + + ); + expect(screen.getByRole('checkbox')).not.toBeChecked(); + + rerender( + + ); + expect(screen.getByRole('checkbox')).toBeChecked(); + }); + + test('calls handleToggleCheckbox when checkbox is clicked', () => { + const mockFn = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('checkbox')); + expect(mockFn).toHaveBeenCalled(); + }); +}); diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.tsx b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.tsx new file mode 100644 index 00000000..c9df0f59 --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/TcvoteFeatureVotesItems.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import TcCheckbox from '../../../../shared/TcCheckbox'; +import { FormControlLabel } from '@mui/material'; +import TcText from '../../../../shared/TcText'; + +interface ITcvoteFeatureVotesItemsProps { + label: string; + color: 'primary' | 'secondary'; + isChecked: boolean; + handleToggleCheckbox: (event: boolean) => void; +} + +function TcvoteFeatureVotesItems({ + label, + color, + isChecked, + handleToggleCheckbox, + ...rest +}: ITcvoteFeatureVotesItemsProps) { + return ( +
+ } + control={ + handleToggleCheckbox(event.target.checked)} + /> + } + /> +
+ ); +} + +export default TcvoteFeatureVotesItems; diff --git a/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/index.ts b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/index.ts new file mode 100644 index 00000000..e293b34c --- /dev/null +++ b/src/components/twitter/growth/voteFeature/TcvoteFeatureVotes/index.ts @@ -0,0 +1 @@ +import { default as TcvoteFeatureVotes } from './TcvoteFeatureVotes'; diff --git a/src/components/twitter/growth/voteFeature/index.ts b/src/components/twitter/growth/voteFeature/index.ts new file mode 100644 index 00000000..7eb05f1b --- /dev/null +++ b/src/components/twitter/growth/voteFeature/index.ts @@ -0,0 +1 @@ +import { default as TcvoteFeature } from './TcvoteFeature'; diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.spec.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.spec.tsx new file mode 100644 index 00000000..3e582e45 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.spec.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcYourAccountActivity from './TcYourAccountActivity'; +import TcYourAccountActivityHeader from './TcYourAccountActivityHeader'; + +describe('', () => { + const mockActivity = { + posts: 10, + likes: 100, + replies: 15, + retweets: 7, + mentions: 8, + }; + + beforeEach(() => { + render(); + }); + + // Test 1: Check if the TcYourAccountActivityHeader component is rendered. + it('renders the header component', () => { + render(); + }); + // Test 2: Check if the data is rendered correctly in the TcYourAccountActivityContent component. + it('renders the correct activity values and descriptions', () => { + expect(screen.getByText('Number of posts')).toBeInTheDocument(); + expect(screen.getByText(mockActivity.posts.toString())).toBeInTheDocument(); + + expect(screen.getByText('Likes')).toBeInTheDocument(); + expect(screen.getByText(mockActivity.likes.toString())).toBeInTheDocument(); + + expect(screen.getByText('Replies')).toBeInTheDocument(); + expect( + screen.getByText(mockActivity.replies.toString()) + ).toBeInTheDocument(); + + expect(screen.getByText('Retweets')).toBeInTheDocument(); + expect( + screen.getByText(mockActivity.retweets.toString()) + ).toBeInTheDocument(); + + expect(screen.getByText('Mentions')).toBeInTheDocument(); + expect( + screen.getByText(mockActivity.mentions.toString()) + ).toBeInTheDocument(); + }); + + // Test 3: Check transformation logic. This might be optional as it's more of an implementation detail. + it('transforms the activity data correctly', () => { + const expectedDescriptions = [ + 'Number of posts', + 'Likes', + 'Replies', + 'Retweets', + 'Mentions', + ]; + + expectedDescriptions.forEach((description) => { + expect(screen.getByText(description)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.tsx new file mode 100644 index 00000000..de2c7607 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivity.tsx @@ -0,0 +1,49 @@ +import React, { useState, useEffect } from 'react'; +import TcYourAccountActivityHeader from './TcYourAccountActivityHeader'; +import TcYourAccountActivityContent from './TcYourAccountActivityContent'; +import { IActivity } from '../../../../utils/interfaces'; +import { capitalizeFirstChar } from '../../../../helpers/helper'; + +interface IAccountActivityItem { + description: string; + value: number; + hasTooltipInfo: boolean; +} + +interface ITcYourAccountActivityProps { + activity: IActivity; +} + +function TcYourAccountActivity({ activity }: ITcYourAccountActivityProps) { + const [yourAccountActivityList, setYourAccountActivityList] = useState< + IAccountActivityItem[] + >([]); + + useEffect(() => { + if (activity) { + const newState = Object.keys(activity).map((key) => { + const activityKey = key as keyof IActivity; + + return { + description: + activityKey === 'posts' + ? 'Number of posts' + : capitalizeFirstChar(activityKey), + value: activity[activityKey], + hasTooltipInfo: false, + }; + }); + + setYourAccountActivityList(newState); + } + }, [activity]); + + return ( +
+ + +
+ ); +} + +export default TcYourAccountActivity; diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.spec.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.spec.tsx new file mode 100644 index 00000000..98716f26 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcYourAccountActivityContent from './TcYourAccountActivityContent'; + +describe('', () => { + const mockData = [ + { description: 'Desc 1', value: 123, hasTooltipInfo: true }, + { description: 'Desc 2', value: 456, hasTooltipInfo: false }, + { description: 'Desc 3', value: 789, hasTooltipInfo: true }, + ]; + + beforeEach(() => { + render(); + }); + + it('renders the correct values and descriptions', () => { + mockData.forEach((item) => { + expect(screen.getByText(item.value.toString())).toBeInTheDocument(); + expect(screen.getByText(item.description)).toBeInTheDocument(); + }); + }); + + it('does not render the TcIconWithTooltip when hasTooltipInfo is false', () => { + // Filtering mockData for items with hasTooltipInfo as false + const itemsWithoutTooltip = mockData.filter((item) => !item.hasTooltipInfo); + + itemsWithoutTooltip.forEach((item) => { + expect( + screen.getByText(item.description).closest('div') + ).not.toHaveTextContent('Followers and non-followers'); + }); + }); +}); diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.tsx new file mode 100644 index 00000000..5e57ded6 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityContent.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import TcCard from '../../../shared/TcCard'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; +import TcText from '../../../shared/TcText'; + +interface IYourAccountActivityContentProps { + data: { + description: string; + value: number; + hasTooltipInfo: boolean; + }[]; +} + +function TcYourAccountActivityContent({ + data, +}: IYourAccountActivityContentProps) { + return ( +
+
+ {data && + data.map((el, index) => ( + + + +
+ {el.hasTooltipInfo ? ( + + ) : null} +
+
+ } + /> + ))} +
+ + ); +} + +export default TcYourAccountActivityContent; diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.spec.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.spec.tsx new file mode 100644 index 00000000..e92f6527 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcYourAccountActivityHeader from './TcYourAccountActivityHeader'; + +describe('', () => { + beforeEach(() => { + render(); + }); + + it('renders the main header text', () => { + const headerText = screen.getByText('Your account activity'); + expect(headerText).toBeInTheDocument(); + expect(headerText.tagName).toBe('H6'); // if MUI's variant h6 is being translated to the HTML h6 tag + }); + + it('renders the subtext about engagement', () => { + const subtext = screen.getByText('How much you engage with others'); + expect(subtext).toBeInTheDocument(); + }); +}); diff --git a/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.tsx b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.tsx new file mode 100644 index 00000000..0f4b2f26 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/TcYourAccountActivityHeader.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import TcText from '../../../shared/TcText'; +import { StorageService } from '../../../../services/StorageService'; +import { IUser } from '../../../../utils/types'; +import TcLink from '../../../shared/TcLink'; + +function TcYourAccountActivityHeader() { + return ( +
+
+ + +
+
+ ); +} + +export default TcYourAccountActivityHeader; diff --git a/src/components/twitter/growth/yourAccountActivity/index.ts b/src/components/twitter/growth/yourAccountActivity/index.ts new file mode 100644 index 00000000..ca1a5011 --- /dev/null +++ b/src/components/twitter/growth/yourAccountActivity/index.ts @@ -0,0 +1 @@ +import { default as TcYourAccountActivity } from './TcYourAccountActivity'; diff --git a/src/helpers/helper.ts b/src/helpers/helper.ts new file mode 100644 index 00000000..dda35967 --- /dev/null +++ b/src/helpers/helper.ts @@ -0,0 +1,15 @@ +import { IDecodedToken } from '../utils/interfaces'; +import { IUser } from '../utils/types'; +import jwt_decode from 'jwt-decode'; + +export function capitalizeFirstChar(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function decodeUserTokenDiscordId(user?: IUser): string | null { + if (user?.token?.accessToken) { + const decodedToken: IDecodedToken = jwt_decode(user.token.accessToken); + return decodedToken.sub; + } + return null; +} diff --git a/src/layouts/defaultLayout.tsx b/src/layouts/defaultLayout.tsx index 2aba59ca..fe71bd8e 100644 --- a/src/layouts/defaultLayout.tsx +++ b/src/layouts/defaultLayout.tsx @@ -1,30 +1,197 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import Sidebar from '../components/layouts/Sidebar'; import SidebarXs from '../components/layouts/xs/SidebarXs'; import useAppStore from '../store/useStore'; import { StorageService } from '../services/StorageService'; import { IUser } from '../utils/types'; +import TcAlert from '../components/shared/TcAlert'; +import TcButton from '../components/shared/TcButton'; +import TcCollapse from '../components/shared/TcCollapse'; +import TcText from '../components/shared/TcText'; +import TcDialog from '../components/shared/TcDialog'; +import { IoCloseSharp } from 'react-icons/io5'; +import TcLink from '../components/shared/TcLink'; +import { useRouter } from 'next/router'; +import jwt_decode from 'jwt-decode'; +import { IDecodedToken } from '../utils/interfaces'; +import { decodeUserTokenDiscordId } from '../helpers/helper'; -type Props = { +type IDefaultLayoutProps = { children: React.ReactNode; }; -export const defaultLayout = ({ children }: Props) => { - const { getGuilds, getGuildInfoByDiscord } = useAppStore(); +export const defaultLayout = ({ children }: IDefaultLayoutProps) => { + const router = useRouter(); + const currentRoute = router.pathname; + + const { getGuilds, getGuildInfoByDiscord, authorizeTwitter, getUserInfo } = + useAppStore(); + const [openDialog, setOpenDialog] = useState(false); + + const user = StorageService.readLocalStorage('user'); useEffect(() => { - const user = StorageService.readLocalStorage('user'); if (user) { const { guildId } = user.guild; getGuilds(); if (guildId) { getGuildInfoByDiscord(guildId); } + const fetchUserInfo = async () => { + const { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + } = await getUserInfo(); + + StorageService.updateLocalStorageWithObject('user', 'twitter', { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + }); + }; + fetchUserInfo(); } }, []); + const handleAuthorizeTwitter = () => { + authorizeTwitter(decodeUserTokenDiscordId(user)); + }; + + const isAllTwitterPropertiesNull = + user && + user.twitter && + Object.values(user.twitter).every((value) => value == null); + return ( <> + {currentRoute === '/growth' && isAllTwitterPropertiesNull && ( + <> + +
+ + setOpenDialog(true)} + sx={{ + border: '1px solid white', + color: 'white', + paddingY: '0', + '&:hover': { + background: 'white', + border: '1px solid white', + color: 'black', + }, + }} + /> +
+ + } + /> + +
+ setOpenDialog(false)} + className="float-right cursor-pointer" + /> +
+
+ +
    +
  1. + + 1 / Go to{' '} + + Twitter + + . Ensure you’re connected with your{' '} + community’s Twitter account and leave this window + open. +

    + } + variant={'body2'} + /> +
  2. +
  3. + + 2 / Once you are connected, click on the button below + “Connect Twitter account” and approve the access. +

    + } + variant={'body2'} + /> +
  4. +
+
+ handleAuthorizeTwitter()} + /> +
+
+
+ + )} +
diff --git a/src/pages/callback.tsx b/src/pages/callback.tsx index 9290b9ed..5e034488 100644 --- a/src/pages/callback.tsx +++ b/src/pages/callback.tsx @@ -5,9 +5,11 @@ import SimpleBackdrop from '../components/global/LoadingBackdrop'; import { StorageService } from '../services/StorageService'; import { toast } from 'react-toastify'; import { BiError } from 'react-icons/bi'; +import useAppStore from '../store/useStore'; export default function callback() { const router = useRouter(); + const { getUserInfo, refreshTwitterMetrics } = useAppStore(); const [loading, toggleLoading] = useState(true); if (typeof window !== 'undefined') { useEffect(() => { @@ -25,10 +27,31 @@ export default function callback() { }, [router]); } - const notify = () => { - toast('Discord authentication faild.please try again.', { - position: 'bottom-left', - autoClose: 3000, + interface NotifyOptions { + message: string; + position?: + | 'top-right' + | 'top-center' + | 'top-left' + | 'bottom-right' + | 'bottom-center' + | 'bottom-left' + | 'top-right'; + autoClose?: number | false; + iconColor?: string; + iconSize?: number; + } + + const notify = ({ + message, + position = 'bottom-left', + autoClose = 3000, + iconColor = '#FB3E56', + iconSize = 40, + }: NotifyOptions) => { + toast(message, { + position, + autoClose, hideProgressBar: true, closeOnClick: false, pauseOnHover: true, @@ -36,7 +59,7 @@ export default function callback() { progress: undefined, closeButton: false, theme: 'light', - icon: , + icon: , }); }; @@ -45,12 +68,12 @@ export default function callback() { let user = StorageService.readLocalStorage('user'); switch (statusCode) { case '490': - notify(); + notify({ message: 'Discord authentication failed. Please try again.' }); router.push('/tryNow'); break; case '491': - notify(); + notify({ message: 'Discord authentication failed. Please try again.' }); router.push('/settings'); break; @@ -197,6 +220,43 @@ export default function callback() { } break; + case '801': + if (user) { + const fetchUserInfo = async () => { + const { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + } = await getUserInfo(); + + StorageService.updateLocalStorageWithObject('user', 'twitter', { + twitterConnectedAt, + twitterId, + twitterProfileImageUrl, + twitterUsername, + }); + + StorageService.writeLocalStorage( + 'lastTwitterMetricsRefreshDate', + new Date().toISOString() + ); + + refreshTwitterMetrics(); + }; + fetchUserInfo(); + router.push({ + pathname: '/growth', + }); + } + break; + case '890': + notify({ message: 'Twitter authorization failed. Please try again.' }); + router.push({ + pathname: '/growth', + }); + break; + default: break; } diff --git a/src/pages/growth.tsx b/src/pages/growth.tsx new file mode 100644 index 00000000..ac297dc3 --- /dev/null +++ b/src/pages/growth.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { defaultLayout } from '../layouts/defaultLayout'; +import SEO from '../components/global/SEO'; +import TcText from '../components/shared/TcText'; +import TcBoxContainer from '../components/shared/TcBox/TcBoxContainer'; +import TcYourAccountActivity from '../components/twitter/growth/yourAccountActivity/TcYourAccountActivity'; +import TcAudienceResponse from '../components/twitter/growth/audienceResponse/TcAudienceResponse'; +import TcEngagementAccounts from '../components/twitter/growth/engagementAccounts/TcEngagementAccounts'; +import useAppStore from '../store/useStore'; +import { StorageService } from '../services/StorageService'; +import { IUser } from '../utils/types'; +import SimpleBackdrop from '../components/global/LoadingBackdrop'; +import { IDataTwitter } from '../utils/interfaces'; +import TcAccountActivity from '../components/twitter/growth/accountActivity/TcAccountActivity'; + +function growth() { + const user = StorageService.readLocalStorage('user'); + + const [data, setData] = useState({ + activity: { + posts: 0, + replies: 0, + retweets: 0, + likes: 0, + mentions: 0, + }, + audience: { + replies: 0, + retweets: 0, + likes: 0, + mentions: 0, + }, + engagement: { + hqla: 0, + hqhe: 0, + lqla: 0, + lqhe: 0, + }, + account: { + follower: 0, + engagement: 0, + }, + }); + + const [loading, setLoading] = useState(false); + + const { + twitterActivityAccount, + twitterAudienceAccount, + twitterEngagementAccount, + twitterAccount, + refreshTwitterMetrics, + } = useAppStore(); + + const updateTwitterMetrics = () => { + const lastTwitterMetricsDateStr = StorageService.readLocalStorage( + 'lastTwitterMetricsRefreshDate', + 'string' + ); + + if (!lastTwitterMetricsDateStr) { + return; + } + + const lastTwitterMetricsDate = new Date(lastTwitterMetricsDateStr); + + const now = new Date(); + const lastRefresh = new Date(lastTwitterMetricsDate); + + const differenceInMillis = now.getTime() - lastRefresh.getTime(); + + if (differenceInMillis >= 24 * 60 * 60 * 1000) { + refreshTwitterMetrics(); + StorageService.writeLocalStorage( + 'lastTwitterMetricsRefreshDate', + new Date().toISOString() + ); + } + }; + + useEffect(() => { + const twitterId = user?.twitter?.twitterId; + + updateTwitterMetrics(); + + setLoading(true); + if (twitterId) { + Promise.all([ + twitterActivityAccount(), + twitterAudienceAccount(), + twitterEngagementAccount(), + twitterAccount(), + ]) + .then( + ([ + activityResponse, + audienceResponse, + engagementResponse, + accountResponse, + ]) => { + setData({ + activity: activityResponse, + audience: audienceResponse, + engagement: engagementResponse, + account: accountResponse, + }); + setLoading(false); + } + ) + .catch((err) => { + setLoading(false); + }); + } + setLoading(false); + }, []); + + if (loading) { + return ; + } + + return ( + <> + +
+ + +
+ } + contentContainerChildren={ +
+ + + + +
+ } + /> +
+ + ); +} + +growth.pageLayout = defaultLayout; + +export default growth; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 80174a84..1dd9cb69 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -81,6 +81,7 @@ function Dashboard(): JSX.Element { variant="filled" onClose={toggleAnalysisState} severity="warning" + sx={{ padding: '6px 9rem 6px 14rem' }} > Data import is in progress. It might take up to 6 hours to finish the data import. Once it is done we will send you a message on diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 86d5d0cf..33b38f62 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -48,6 +48,7 @@ function Settings(): JSX.Element { fetchEmail(); const intervalId = setInterval(() => { getGuilds(); + getUserInfo(); }, 5000); // Clean up the interval when the component unmounts diff --git a/src/services/StorageService.ts b/src/services/StorageService.ts index 97563047..2148b180 100644 --- a/src/services/StorageService.ts +++ b/src/services/StorageService.ts @@ -33,4 +33,25 @@ export class StorageService { public static removeLocalStorage(key: string): void { localStorage.removeItem(STORAGE_PREFIX + key); } + public static updateLocalStorageWithObject( + key: string, + newObjectKey: string, + newObject: Record + ): void { + const currentObj = this.readLocalStorage(key); + + if (!currentObj || typeof currentObj !== 'object') { + console.error('Current value is not an object, or it does not exist'); + return; + } + + if (typeof newObject !== 'object' || Array.isArray(newObject)) { + console.error('newObject should be an object and not an array.'); + return; + } + + (currentObj as any)[newObjectKey] = newObject; + + this.writeLocalStorage(key, currentObj); + } } diff --git a/src/store/slices/settingSlice.ts b/src/store/slices/settingSlice.ts index e60fc206..175ede5d 100644 --- a/src/store/slices/settingSlice.ts +++ b/src/store/slices/settingSlice.ts @@ -25,13 +25,10 @@ const createSettingSlice: StateCreator = (set, get) => ({ }, getUserInfo: async () => { try { - set(() => ({ isLoading: true })); const { data } = await axiosInstance.get('/users/@me'); - set({ userInfo: data, isLoading: false }); + set({ userInfo: data }); return data; - } catch (error) { - set(() => ({ isLoading: false })); - } + } catch (error) {} }, getGuildInfoByDiscord: async (guildId) => { try { diff --git a/src/store/slices/twitterSlice.ts b/src/store/slices/twitterSlice.ts new file mode 100644 index 00000000..6175ea82 --- /dev/null +++ b/src/store/slices/twitterSlice.ts @@ -0,0 +1,52 @@ +import { StateCreator } from 'zustand'; +import { axiosInstance } from '../../axiosInstance'; +import ITwitter from '../types/ITwitter'; +import { conf } from '../../configs'; + +const BASE_URL = conf.API_BASE_URL; + +const createTwitterSlice: StateCreator = (set, get) => ({ + authorizeTwitter: async (discordId: string) => { + try { + location.replace(`${BASE_URL}/auth/twitter/login/user/${discordId}`); + } catch (error) { + console.error('Error in intermediary auth step:', error); + } + }, + disconnectTwitter: async () => { + try { + await axiosInstance.post(`twitter/disconnect`); + } catch (error) {} + }, + refreshTwitterMetrics: async () => { + try { + await axiosInstance.post(`/twitter/metrics/refresh`); + } catch (error) {} + }, + twitterActivityAccount: async () => { + try { + const { data } = await axiosInstance.get(`/twitter/metrics/activity`); + return data; + } catch (error) {} + }, + twitterAudienceAccount: async () => { + try { + const { data } = await axiosInstance.get(`/twitter/metrics/audience`); + return data; + } catch (error) {} + }, + twitterEngagementAccount: async () => { + try { + const { data } = await axiosInstance.get(`/twitter/metrics/engagement`); + return data; + } catch (error) {} + }, + twitterAccount: async () => { + try { + const { data } = await axiosInstance.get(`/twitter/metrics/account`); + return data; + } catch (error) {} + }, +}); + +export default createTwitterSlice; diff --git a/src/store/types/ISetting.ts b/src/store/types/ISetting.ts index 82ff978d..5de57107 100644 --- a/src/store/types/ISetting.ts +++ b/src/store/types/ISetting.ts @@ -11,6 +11,19 @@ export type IGuildInfo = { export type DISCONNECT_TYPE = 'soft' | 'hard'; +export interface IUserInfo { + discordId: string; + email: string; + verified: boolean; + avatar: string; + twitterConnectedAt: string; + twitterId: string; + twitterProfileImageUrl: string; + twitterUsername: string; + twitterIsInProgress: boolean; + id: string; +} + export default interface IGuildList extends IGuildInfo { isInProgress?: boolean; isDisconnected?: boolean; @@ -20,7 +33,7 @@ export default interface ISetting { isLoading: boolean; isRefetchLoading: boolean; guildInfo?: IGuildInfo | {}; - userInfo: {}; + userInfo: IUserInfo | {}; guildInfoByDiscord: {}; guilds: IGuildList[]; guildChannels: IGuildChannels[]; diff --git a/src/store/types/ITwitter.ts b/src/store/types/ITwitter.ts new file mode 100644 index 00000000..a817cb73 --- /dev/null +++ b/src/store/types/ITwitter.ts @@ -0,0 +1,9 @@ +export default interface ITwitter { + authorizeTwitter: (discordId: string) => void; + disconnectTwitter: () => void; + refreshTwitterMetrics: () => void; + twitterActivityAccount: () => void; + twitterAudienceAccount: () => void; + twitterEngagementAccount: () => void; + twitterAccount: () => void; +} diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 18401a43..a057bc9c 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -5,6 +5,7 @@ import createSettingSlice from './slices/settingSlice'; import createBreakdownsSlice from './slices/breakdownsSlice'; import createMemberInteractionSlice from './slices/memberInteractionSlice'; import communityHealthSlice from './slices/communityHealthSlice'; +import twitterSlice from './slices/twitterSlice'; const useAppStore = create()((...a) => ({ ...createAuthSlice(...a), @@ -13,6 +14,7 @@ const useAppStore = create()((...a) => ({ ...createBreakdownsSlice(...a), ...createMemberInteractionSlice(...a), ...communityHealthSlice(...a), + ...twitterSlice(...a), })); export default useAppStore; diff --git a/src/styles/globals.css b/src/styles/globals.css index 90cdbcb3..a09541cc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -57,6 +57,16 @@ body { line-height: 24px; } +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge, and Firefox */ +.scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + .css-1hv8oq8-MuiStepLabel-label.MuiStepLabel-alternativeLabel { font-size: 16px !important; line-height: 24px !important; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 829933ce..de526a19 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -85,3 +85,37 @@ export interface ITrackEventParams { eventProperties?: Record; callback?: (result: { event: any; code: any; message: any }) => void; } + +export interface IActivity { + posts: number; + replies: number; + retweets: number; + likes: number; + mentions: number; +} + +export interface IAudience { + replies: number; + retweets: number; + likes: number; + mentions: number; +} + +export interface IEngagement { + hqla: number; + hqhe: number; + lqla: number; + lqhe: number; +} + +export interface IAccount { + follower: number; + engagement: number; +} + +export interface IDataTwitter { + activity: IActivity; + audience: IAudience; + engagement: IEngagement; + account: IAccount; +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 5f34779f..91ad69d7 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -7,6 +7,15 @@ export const theme = createTheme({ }, }, typography: { + fontFamily: 'inherit', + fontWeightBold: '500', + fontWeightExtraBold: '700', + h3: { + fontSize: '2.5rem', + }, + h4: { + fontSize: '1.75rem', + }, button: { textTransform: 'none', }, @@ -15,11 +24,32 @@ export const theme = createTheme({ MuiButton: { styleOverrides: { root: { - borderRadius: '4px', + borderRadius: '8px', color: '#804EE1', + minWidth: '15rem', + padding: '0.5rem', '&.Mui-disabled': { opacity: 0.7, }, + '@media (max-width:1023px)': { + minWidth: '100%', + }, + }, + contained: { + background: '#804EE1 !important', + color: 'white', + '&.Mui-disabled': { + color: 'white', + }, + }, + outlined: { + background: 'transparent', + border: '1px solid #222222', + color: '#222222', + '&:hover': { + background: '#F5F5F5', + border: '1px solid #222222', + }, }, }, }, @@ -54,7 +84,6 @@ export const theme = createTheme({ MuiAlert: { styleOverrides: { root: { - padding: '6px 9rem 6px 14rem', borderRadius: '0px', position: 'sticky', top: '0', @@ -107,7 +136,15 @@ export const theme = createTheme({ }, }, }); +declare module '@mui/material/styles/createTypography' { + interface TypographyOptions { + fontWeightExtraBold?: string; + } + interface Typography { + fontWeightExtraBold: string; + } +} declare module '@mui/material/styles' { interface Palette { neutral: Palette['primary']; diff --git a/src/utils/types.ts b/src/utils/types.ts index 5fa543cd..0acee40f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -14,9 +14,19 @@ export interface callbackUrlParams extends IGuild, IToken { statusCode: number | string; } +export interface ITwitter { + twitterConnectedAt: string; + twitterId: string; + twitterProfileImageUrl: string; + twitterUsername: string; + lastUpdatedMetrics: string; + twitterIsInProgress: boolean; +} + export type IUser = { token: IToken; guild: IGuild; + twitter?: ITwitter; }; export type IGuildChannels = { diff --git a/tailwind.config.js b/tailwind.config.js index d5e68aab..6d49e111 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -20,13 +20,7 @@ const colors = { 500: '#225262', }, info: { - DEFAULT: '#4368F1', - 50: '#F7FFFE', - 100: '#D0FBF8', - 200: '#A7F3F0', - 300: '#92DAD6', - 400: '#7DC0BD', - 500: '#39C2C0', + DEFAULT: '#1DA1F2', 600: '#313671', }, error: { @@ -80,7 +74,8 @@ const colors = { 'purple-dark': '#673FB5', 'purple-darker': '#35205E', 'gray-subtitle':'#767676', - orange:'#FF8022' + orange:'#FF8022', + 'gray-border-box':'#AAAAAA' }; const backgroundImage = {