├── node.txt ├── src ├── tests │ └── constants │ │ ├── index.ts │ │ └── global.ts ├── react-app-env.d.ts ├── hooks │ ├── index.ts │ └── useQuery.ts ├── components │ ├── UI │ │ ├── Link │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── Link.tsx │ │ │ └── __tests │ │ │ │ └── Link.test.tsx │ │ ├── Avatar │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── AvatarContainer.tsx │ │ │ ├── Avatar.module.scss │ │ │ └── Avatar.tsx │ │ ├── Button │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── Button.tsx │ │ │ └── __tests │ │ │ │ └── Button.test.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── Input.tsx │ │ │ └── __tests__ │ │ │ │ └── Input.test.tsx │ │ ├── Loader │ │ │ ├── index.tsx │ │ │ └── Loader.tsx │ │ ├── Container │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── Container.tsx │ │ └── index.ts │ ├── shared │ │ ├── index.ts │ │ └── Icon │ │ │ ├── icons │ │ │ ├── index.ts │ │ │ ├── CheckMark.tsx │ │ │ └── UserAstronaut.tsx │ │ │ ├── types.ts │ │ │ └── index.tsx │ ├── layouts │ │ ├── BaseLayout │ │ │ ├── BaseLayout.module.scss │ │ │ ├── index.ts │ │ │ ├── components │ │ │ │ ├── Header │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── HeaderContainer.tsx │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── Header.test.tsx │ │ │ │ │ ├── Header.module.scss │ │ │ │ │ └── Header.tsx │ │ │ │ └── LoginSuggesting │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── LogginSuggestions.module.scss │ │ │ │ │ └── LoginSuggesting.tsx │ │ │ ├── types.ts │ │ │ ├── BaseLayoutContainer.tsx │ │ │ ├── useBaseLayoutContainer.ts │ │ │ ├── __tests__ │ │ │ │ └── BaseLayout.test.tsx │ │ │ └── BaseLayout.tsx │ │ ├── AuthLayout │ │ │ ├── index.ts │ │ │ ├── AuthLayoutContainer.tsx │ │ │ ├── AuthLayout.module.scss │ │ │ └── AuthLayout.tsx │ │ └── index.ts │ └── index.ts ├── pages │ ├── index.ts │ ├── user │ │ ├── Home │ │ │ ├── index.ts │ │ │ ├── components │ │ │ │ └── SubscriptionCard │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── SubscriptionCardFeauture.module.scss │ │ │ │ │ └── SubscriptionCardFeauture.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── useSubscriptionCardContainer.tsx │ │ │ │ │ ├── SubscriptionCardContainer.tsx │ │ │ │ │ ├── __tests__ │ │ │ │ │ └── SubscriptionCard.test.tsx │ │ │ │ │ ├── SubscriptionCard.tsx │ │ │ │ │ └── SubscriptionCard.module.scss │ │ │ ├── types.ts │ │ │ ├── HomeContainer.tsx │ │ │ ├── Home.module.scss │ │ │ ├── Home.tsx │ │ │ ├── useHomeContainer.tsx │ │ │ └── __tests__ │ │ │ │ └── Home.test.tsx │ │ ├── Success │ │ │ ├── index.ts │ │ │ ├── SuccessContainer.tsx │ │ │ ├── Success.module.scss │ │ │ ├── __tests__ │ │ │ │ └── Success.test.tsx │ │ │ ├── useSuccessContainer.ts │ │ │ └── Success.tsx │ │ └── index.ts │ └── guest │ │ ├── Auth │ │ ├── index.ts │ │ ├── types.ts │ │ ├── AuthContainer.tsx │ │ ├── Auth.module.scss │ │ ├── useAuthContainer.ts │ │ ├── __tests__ │ │ │ └── Auth.test.tsx │ │ └── Auth.tsx │ │ ├── VerifyEmail │ │ ├── index.ts │ │ ├── types.ts │ │ ├── VerifyEmail.module.scss │ │ ├── VerifyEmailContainer.tsx │ │ ├── __tests__ │ │ │ └── VerifyEmail.test.tsx │ │ ├── VerifyEmail.tsx │ │ └── userVerifyEmailContainer.ts │ │ ├── ResetPassword │ │ ├── index.ts │ │ ├── types.ts │ │ ├── ResetPasswordContainer.tsx │ │ ├── useResetPasswordContainer.tsx │ │ ├── ResetPassword.module.scss │ │ └── ResetPassword.tsx │ │ ├── ForgotPassword │ │ ├── index.ts │ │ ├── types.ts │ │ ├── ForgotPasswordContainer.tsx │ │ ├── useForgotPassordContainer.ts │ │ ├── ForgotPassword.module.scss │ │ └── ForgotPassword.tsx │ │ └── index.ts ├── App │ ├── index.ts │ ├── types.ts │ ├── AppContainer.tsx │ ├── useAppContainer.ts │ └── App.tsx ├── store │ ├── plan │ │ ├── plan.actionTypes.ts │ │ ├── types.ts │ │ ├── plan.selectors.ts │ │ ├── plan.action.ts │ │ └── plan.reducer.ts │ ├── ui │ │ ├── ui.actionTypes.ts │ │ ├── types.ts │ │ ├── ui.actions.ts │ │ └── ui.reducer.ts │ ├── user │ │ ├── user.actionTypes.ts │ │ ├── user.selectors.ts │ │ ├── types.ts │ │ ├── user.reducer.ts │ │ └── user.action.ts │ ├── activeSubscription │ │ ├── activeSubscription.actionTypes.ts │ │ ├── activeSubscription.reducer.ts │ │ ├── types.ts │ │ └── activeSubscription.action.ts │ ├── reducers.ts │ ├── index.ts │ └── cofigureStore.ts ├── constants │ ├── index.ts │ ├── messages.ts │ └── validation │ │ ├── types.ts │ │ └── index.ts ├── types │ ├── index.ts │ ├── activeSubscirption.ts │ ├── global.ts │ ├── plan.ts │ └── user.ts ├── services │ ├── Service.ts │ ├── types.ts │ ├── PlanService.ts │ ├── ActiveSubscriptionService.ts │ └── UserService.ts ├── assets │ └── styles │ │ ├── _global.scss │ │ ├── components │ │ ├── _container.scss │ │ ├── _loader.scss │ │ ├── _input.scss │ │ └── _button.scss │ │ ├── _main.scss │ │ ├── index.scss │ │ ├── abstracts │ │ ├── _mixins.scss │ │ └── _variables.scss │ │ └── _reset.scss ├── utils │ ├── index.ts │ ├── schemas │ │ ├── index.ts │ │ ├── forgotPasswordSchema.ts │ │ ├── signInSchema.ts │ │ ├── resetPasswordSchema.ts │ │ └── signUpSchema.ts │ ├── getPreferredTheme │ │ └── index.ts │ └── request │ │ └── index.ts ├── setupTests.ts ├── lib │ └── stripe.ts ├── reportWebVitals.ts ├── index.tsx ├── service-worker.ts └── serviceWorkerRegistration.ts ├── .env.example ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── images │ ├── logo.png │ └── payment-successful-illustration.jpg ├── manifest.json └── index.html ├── .prettierrc.js ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── github-actions-demo.yml ├── .eslintrc.js └── package.json /node.txt: -------------------------------------------------------------------------------- 1 | node v18.16.1 2 | npm v9.5.7 3 | yarn v1.22.19 -------------------------------------------------------------------------------- /src/tests/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './global'; 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL= 2 | REACT_APP_STRIPE_PUBLISHABLE_KEY= -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useQuery } from './useQuery'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Link/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Link } from './Link'; 2 | -------------------------------------------------------------------------------- /src/components/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Icon } from './Icon'; 2 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | export * from './guest'; 3 | -------------------------------------------------------------------------------- /src/App/index.ts: -------------------------------------------------------------------------------- 1 | import App from './AppContainer'; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /src/components/UI/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input'; 2 | -------------------------------------------------------------------------------- /src/pages/user/Home/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Home } from './HomeContainer'; 2 | -------------------------------------------------------------------------------- /src/store/plan/plan.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export const GET_PLANS = 'plan/GET_PLANS'; 2 | -------------------------------------------------------------------------------- /src/store/ui/ui.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export const CHANGE_THEME = 'ui/CHANGE_THEME'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Loader } from './Loader'; 2 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validation'; 2 | export * from './messages'; 3 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Auth } from './AuthContainer'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Container/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Container } from './Container'; 2 | -------------------------------------------------------------------------------- /src/pages/user/Success/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Success } from './SuccessContainer'; 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/BaseLayout.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | padding-top: 4.5rem; 3 | } -------------------------------------------------------------------------------- /src/pages/user/index.ts: -------------------------------------------------------------------------------- 1 | export { Home } from './Home'; 2 | export { Success } from './Success'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertArakelyan/checkinator-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertArakelyan/checkinator-frontend/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertArakelyan/checkinator-frontend/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/index.ts: -------------------------------------------------------------------------------- 1 | export { default as VerifyEmail } from './VerifyEmailContainer'; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UI'; 2 | export * from './layouts'; 3 | export * from './shared'; 4 | -------------------------------------------------------------------------------- /src/components/layouts/AuthLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AuthLayout } from './AuthLayoutContainer'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BaseLayout } from './BaseLayoutContainer'; 2 | -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResetPassword } from './ResetPasswordContainer'; 2 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertArakelyan/checkinator-frontend/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /src/constants/messages.ts: -------------------------------------------------------------------------------- 1 | export const globalMessages = { 2 | smthWentWrong: 'Something went wrong.', 3 | }; 4 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ForgotPassword } from './ForgotPasswordContainer'; 2 | -------------------------------------------------------------------------------- /src/App/types.ts: -------------------------------------------------------------------------------- 1 | import { ThemeType } from 'types'; 2 | 3 | export interface IAppProps { 4 | theme: ThemeType; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './HeaderContainer'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseLayout } from './BaseLayout'; 2 | export { AuthLayout } from './AuthLayout'; 3 | -------------------------------------------------------------------------------- /src/store/ui/types.ts: -------------------------------------------------------------------------------- 1 | import { ThemeType } from 'types'; 2 | 3 | export interface IUIState { 4 | theme: ThemeType; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/LoginSuggesting/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LogginSuggesting } from './LoginSuggesting'; 2 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SubscriptionCard } from './SubscriptionCardContainer'; 2 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/types.ts: -------------------------------------------------------------------------------- 1 | import { UseAuthContainerType } from './useAuthContainer'; 2 | 3 | export type IAuthProps = UseAuthContainerType; 4 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface ISubscriptionCardFeatureProps { 2 | value: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/user/Home/types.ts: -------------------------------------------------------------------------------- 1 | import { UseHomeContainerType } from './useHomeContainer'; 2 | 3 | export type IHomeProps = UseHomeContainerType; 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './global'; 2 | export * from './user'; 3 | export * from './plan'; 4 | export * from './activeSubscirption'; 5 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SubscriptionCardFeature } from './SubscriptionCardFeauture'; 2 | -------------------------------------------------------------------------------- /src/components/UI/Avatar/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarProps { 2 | src?: string; 3 | } 4 | 5 | export type IAvatarContainerProps = IAvatarProps; 6 | -------------------------------------------------------------------------------- /src/services/Service.ts: -------------------------------------------------------------------------------- 1 | class Service { 2 | static catchError(error: any) { 3 | console.log(error); 4 | } 5 | } 6 | 7 | export default Service; 8 | -------------------------------------------------------------------------------- /src/components/shared/Icon/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as UserAstronaout } from './UserAstronaut'; 2 | export { default as CheckMark } from './CheckMark'; 3 | -------------------------------------------------------------------------------- /src/services/types.ts: -------------------------------------------------------------------------------- 1 | export interface IResponseData { 2 | data: T; 3 | message: string; 4 | success: boolean; 5 | statusCode: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/styles/_global.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: underline; 3 | color: var(--link); 4 | 5 | &--primary { 6 | color: var(--primary); 7 | } 8 | } -------------------------------------------------------------------------------- /src/assets/styles/components/_container.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | } 5 | 6 | .container-fluid { 7 | padding: 0 1rem; 8 | } -------------------------------------------------------------------------------- /public/images/payment-successful-illustration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlbertArakelyan/checkinator-frontend/HEAD/public/images/payment-successful-illustration.jpg -------------------------------------------------------------------------------- /src/store/user/user.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export const SIGN_UP = 'user/SIGN_UP'; 2 | export const VERIFY_EMAIL = 'user/VERIFY_EMAIL'; 3 | export const SIGN_IN = 'user/SIGN_IN'; 4 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getPreferredTheme } from './getPreferredTheme'; 2 | export * from './schemas'; 3 | export { default as request } from './request'; 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | printWidth: 120, 5 | singleQuote: true, 6 | semi: true, 7 | jsxSingleQuote: false, 8 | }; -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/types.ts: -------------------------------------------------------------------------------- 1 | import { UseVerifyEmailContainerType } from './userVerifyEmailContainer'; 2 | 3 | export type IVerifyEmailProps = UseVerifyEmailContainerType; 4 | -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/types.ts: -------------------------------------------------------------------------------- 1 | import { UseResetPasswordContainerType } from './useResetPasswordContainer'; 2 | 3 | export type IResetPasswordProps = UseResetPasswordContainerType; 4 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/types.ts: -------------------------------------------------------------------------------- 1 | import { UseForgotPasswordContainerType } from './useForgotPassordContainer'; 2 | 3 | export type IForgotPasswordProps = UseForgotPasswordContainerType; 4 | -------------------------------------------------------------------------------- /src/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | const useQuery = () => { 4 | return new URLSearchParams(useLocation().search); 5 | }; 6 | 7 | export default useQuery; 8 | -------------------------------------------------------------------------------- /src/constants/validation/types.ts: -------------------------------------------------------------------------------- 1 | export interface IValidationLength { 2 | short: 64; 3 | base: 128; 4 | long: 256; 5 | } 6 | 7 | export interface IMinValidationLength { 8 | base: 8; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/guest/index.ts: -------------------------------------------------------------------------------- 1 | export { Auth } from './Auth'; 2 | export { ForgotPassword } from './ForgotPassword'; 3 | export { ResetPassword } from './ResetPassword'; 4 | export { VerifyEmail } from './VerifyEmail'; 5 | -------------------------------------------------------------------------------- /src/components/layouts/AuthLayout/AuthLayoutContainer.tsx: -------------------------------------------------------------------------------- 1 | import AuthLayout from './AuthLayout'; 2 | 3 | const AuthLayoutContainer = () => { 4 | return ; 5 | }; 6 | 7 | export default AuthLayoutContainer; 8 | -------------------------------------------------------------------------------- /src/components/UI/Container/types.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, HTMLAttributes } from 'react'; 2 | 3 | export interface IContainerProps extends PropsWithChildren, HTMLAttributes { 4 | fluid?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/styles/_main.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: sans-serif; 3 | font-size: 16px; 4 | } 5 | 6 | .App { 7 | background-color: var(--bg-color); 8 | color: var(--primary-text-color); 9 | min-height: 100vh; 10 | } -------------------------------------------------------------------------------- /src/types/activeSubscirption.ts: -------------------------------------------------------------------------------- 1 | export interface IActiveSubscriptionData { 2 | name: string; 3 | id: string; 4 | price: number; 5 | } 6 | 7 | export interface IActivatedSubscriptionData { 8 | planId: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/store/activeSubscription/activeSubscription.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export const CREATE_CHECKOUT_SESSION = 'activeSubscription/CREATE_CHECKOUT_SESSION'; 2 | export const CREATE_ACTIVE_SUBSCRIPTION = 'activeSubscription/CREATE_ACTIVE_SUBSCRIPTION'; 3 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/types.ts: -------------------------------------------------------------------------------- 1 | import { AccessTokenType } from 'types'; 2 | 3 | export interface IHeaderContainerProps { 4 | accessToken: AccessTokenType; 5 | } 6 | 7 | export type IHeaderProps = IHeaderContainerProps; 8 | -------------------------------------------------------------------------------- /src/store/ui/ui.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | 3 | import { CHANGE_THEME } from './ui.actionTypes'; 4 | 5 | import { ThemeType } from 'types'; 6 | 7 | export const changeTheme = createAction(CHANGE_THEME); 8 | -------------------------------------------------------------------------------- /src/components/UI/index.ts: -------------------------------------------------------------------------------- 1 | export { Container } from './Container'; 2 | export { Avatar } from './Avatar'; 3 | export { Button } from './Button'; 4 | export { Link } from './Link'; 5 | export { Input } from './Input'; 6 | export { Loader } from './Loader'; 7 | -------------------------------------------------------------------------------- /src/store/plan/types.ts: -------------------------------------------------------------------------------- 1 | import { IPlan } from 'types'; 2 | 3 | export interface IPlanState { 4 | list: IPlan[]; 5 | loading: boolean; 6 | error: null | string; 7 | } 8 | 9 | // getPlans action 10 | export type GetPlansReturnDataType = IPlan[]; 11 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/components/UI/Link/types.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { LinkProps } from 'react-router-dom'; 3 | import { VariantType } from 'types'; 4 | 5 | export interface ILinkProps extends PropsWithChildren, LinkProps { 6 | variant?: VariantType; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/types.ts: -------------------------------------------------------------------------------- 1 | import { AccessTokenType } from 'types'; 2 | 3 | export interface IUseBaseLayoutContainerReturnData { 4 | accessToken: AccessTokenType; 5 | } 6 | 7 | export interface IBaseLayoutProps { 8 | accessToken: AccessTokenType; 9 | } 10 | -------------------------------------------------------------------------------- /src/App/AppContainer.tsx: -------------------------------------------------------------------------------- 1 | import useAppContainer from './useAppContainer'; 2 | 3 | import App from './App'; 4 | 5 | const AppContainer = () => { 6 | const { theme } = useAppContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default AppContainer; 12 | -------------------------------------------------------------------------------- /src/store/plan/plan.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | 3 | import { RootState } from 'store/cofigureStore'; 4 | 5 | const planState = (state: RootState) => state.plan; 6 | 7 | export const selectPlansList = createSelector(planState, ({ list }) => list); 8 | -------------------------------------------------------------------------------- /src/types/global.ts: -------------------------------------------------------------------------------- 1 | type SubscriptionCardVariant = 'subscription-purple' | 'subscription-blue'; 2 | 3 | export type VariantType = 'primary' | 'secondary' | SubscriptionCardVariant; 4 | 5 | export type ThemeType = 'light' | 'dark'; 6 | 7 | export type AccessTokenType = string | null; 8 | -------------------------------------------------------------------------------- /src/components/UI/Input/types.ts: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from 'react'; 2 | import { VariantType } from 'types'; 3 | 4 | export interface IInputProps extends InputHTMLAttributes { 5 | wrapperClassName?: string; 6 | variant?: VariantType; 7 | error?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export { default as signInSchema } from './signInSchema'; 2 | export { default as signUpSchema } from './signUpSchema'; 3 | export { default as forgotPasswordSchema } from './forgotPasswordSchema'; 4 | export { default as resetPasswordSchema } from './resetPasswordSchema'; 5 | -------------------------------------------------------------------------------- /src/pages/user/Success/SuccessContainer.tsx: -------------------------------------------------------------------------------- 1 | import Success from './Success'; 2 | 3 | import useSuccessContainer from './useSuccessContainer'; 4 | 5 | const SuccessContainer = () => { 6 | useSuccessContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default SuccessContainer; 12 | -------------------------------------------------------------------------------- /src/components/UI/Button/types.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, PropsWithChildren } from 'react'; 2 | 3 | import { VariantType } from 'types'; 4 | 5 | export interface IButtonProps extends PropsWithChildren, ButtonHTMLAttributes { 6 | variant?: VariantType; 7 | isLoading?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/getPreferredTheme/index.ts: -------------------------------------------------------------------------------- 1 | import { ThemeType } from 'types'; 2 | 3 | const getPreferredTheme = (): ThemeType => { 4 | const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 5 | 6 | return isDark ? 'dark' : 'light'; 7 | }; 8 | 9 | export default getPreferredTheme; 10 | -------------------------------------------------------------------------------- /src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import uiReducer from './ui/ui.reducer'; 2 | import userReducer from './user/user.reducer'; 3 | import planReducer from './plan/plan.reducer'; 4 | 5 | const reducers = { 6 | ui: uiReducer, 7 | user: userReducer, 8 | plan: planReducer, 9 | }; 10 | 11 | export default reducers; 12 | -------------------------------------------------------------------------------- /src/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import { loadStripe } from '@stripe/stripe-js'; 2 | 3 | let stripePromise: any; 4 | 5 | export const getStripe = () => { 6 | if (!stripePromise) { 7 | stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY as string); 8 | } 9 | 10 | return stripePromise; 11 | }; 12 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; 2 | 3 | // Store 4 | import { RootState, AppDispatch } from './cofigureStore'; 5 | 6 | export const useAppDispatch = () => useDispatch(); 7 | 8 | export const useAppSelector: TypedUseSelectorHook = useSelector; 9 | -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "./reset"; 2 | 3 | // Abstracts 4 | @import "./abstracts/variables"; 5 | @import "./abstracts/mixins"; 6 | 7 | // Components 8 | @import "./components/container"; 9 | @import "./components/button"; 10 | @import "./components/input"; 11 | 12 | // Main 13 | @import "./global"; 14 | @import "./main"; -------------------------------------------------------------------------------- /src/components/UI/Avatar/AvatarContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import Avatar from './Avatar'; 4 | 5 | import { IAvatarContainerProps } from './types'; 6 | 7 | const AvatarContainer: FC = ({ src }) => { 8 | return ; 9 | }; 10 | 11 | export default AvatarContainer; 12 | -------------------------------------------------------------------------------- /src/pages/user/Home/HomeContainer.tsx: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | import useHomeContainer from './useHomeContainer'; 4 | 5 | const HomeContainer = () => { 6 | const { plansListContent } = useHomeContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default HomeContainer; 12 | -------------------------------------------------------------------------------- /src/services/PlanService.ts: -------------------------------------------------------------------------------- 1 | import Service from './Service'; 2 | 3 | import { request } from 'utils'; 4 | 5 | import { IResponseData } from './types'; 6 | 7 | class PlanService extends Service { 8 | static getPlans() { 9 | return request>('GET', 'plan'); 10 | } 11 | } 12 | 13 | export default PlanService; 14 | -------------------------------------------------------------------------------- /src/store/activeSubscription/activeSubscription.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | const initialState = {}; 4 | 5 | const activeSubscriptionReducer = createReducer(initialState, (builder) => { 6 | builder.addDefaultCase((state) => state); 7 | }); 8 | 9 | export default activeSubscriptionReducer; 10 | -------------------------------------------------------------------------------- /src/store/activeSubscription/types.ts: -------------------------------------------------------------------------------- 1 | import { IActiveSubscriptionData, IActivatedSubscriptionData } from 'types'; 2 | 3 | // createCheckoutSession action 4 | export type ICreateCheckoutSessionPayloadData = IActiveSubscriptionData; 5 | 6 | // createActiveSubscription action 7 | export type ICreateActiveSubscriptionPayloadData = IActivatedSubscriptionData; 8 | -------------------------------------------------------------------------------- /src/components/UI/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default Loader; 12 | -------------------------------------------------------------------------------- /src/components/shared/Icon/types.ts: -------------------------------------------------------------------------------- 1 | import { icons } from './index'; 2 | 3 | export type IconsType = typeof icons; 4 | 5 | export interface IIconComponentProps { 6 | width?: number; 7 | height?: number; 8 | color?: string; 9 | className?: string; 10 | } 11 | 12 | export interface IIconProps extends IIconComponentProps { 13 | name: keyof IconsType; 14 | } 15 | -------------------------------------------------------------------------------- /src/store/cofigureStore.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | // Reducers 4 | import reducers from './reducers'; 5 | 6 | const store = configureStore({ 7 | reducer: reducers, 8 | }); 9 | 10 | export type RootState = ReturnType; 11 | export type AppDispatch = typeof store.dispatch; 12 | 13 | export default store; 14 | -------------------------------------------------------------------------------- /src/types/plan.ts: -------------------------------------------------------------------------------- 1 | export interface IPlanItem { 2 | name: string; 3 | _id: string; 4 | created_at: string; 5 | updated_at: string; 6 | } 7 | 8 | export interface IPlan { 9 | _id: string; 10 | name: string; 11 | price: number; 12 | planItems: IPlanItem[]; 13 | color: string; // VariantType maybe 14 | created_at: string; 15 | updated_at: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/BaseLayoutContainer.tsx: -------------------------------------------------------------------------------- 1 | import BaseLayout from './BaseLayout'; 2 | 3 | import useBaseLayoutContainer from './useBaseLayoutContainer'; 4 | 5 | const BaseLayoutContainer = () => { 6 | const { accessToken } = useBaseLayoutContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default BaseLayoutContainer; 12 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/HeaderContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import Header from './Header'; 4 | 5 | import { IHeaderContainerProps } from './types'; 6 | 7 | const HeaderContainer: FC = ({ accessToken }) => { 8 | return ; 9 | }; 10 | 11 | export default HeaderContainer; 12 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/__tests__/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import Header from '../Header'; 4 | 5 | describe('Header', () => { 6 | it('should have one h1 logo', () => { 7 | const { container } = render(); 8 | 9 | expect(container.querySelectorAll('h1').length).toBe(1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/layouts/AuthLayout/AuthLayout.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .auth-layout { 5 | min-height: 100vh; 6 | padding: 0 1rem; 7 | @include flex(column, center, center); 8 | } 9 | .auth-layout__container { 10 | max-width: 26.25rem; 11 | width: 100%; 12 | border-radius: 0.375rem; 13 | padding: 1rem; 14 | @include border(all, var(--primary)); 15 | } -------------------------------------------------------------------------------- /src/components/UI/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { IContainerProps } from './types'; 4 | 5 | const Container: FC = ({ children, fluid, className, ...props }) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export default Container; 14 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/LoginSuggesting/LogginSuggestions.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../assets//styles/abstracts/mixins"; 2 | 3 | .login-suggesting { 4 | @include flex(column, center, center); 5 | min-height: calc(100vh - 4.5rem); 6 | } 7 | .login-suggesting__title { 8 | font-size: 2rem; 9 | margin-bottom: 1rem; 10 | } 11 | .login-suggesting__button { 12 | font-size: 1.5rem; 13 | } -------------------------------------------------------------------------------- /src/components/layouts/AuthLayout/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | 3 | import styles from './AuthLayout.module.scss'; 4 | 5 | const AuthLayout = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default AuthLayout; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .idea 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/components/UI/Avatar/Avatar.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .avatar-container { 5 | border-radius: 50%; 6 | width: 2.5rem; 7 | height: 2.5rem; 8 | // background-color: var(--light-gray); 9 | @include flex(row, center, center); 10 | @include border(all, var(--primary), 0.25rem); 11 | } 12 | .avatar { 13 | display: block; 14 | width: 100%; 15 | height: 100%; 16 | border-radius: 50%; 17 | } -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/ResetPasswordContainer.tsx: -------------------------------------------------------------------------------- 1 | import ResetPassword from './ResetPassword'; 2 | 3 | import useResetPasswordContainer from './useResetPasswordContainer'; 4 | 5 | const ResetPasswordContainer = () => { 6 | const { register, handleSubmit, errors } = useResetPasswordContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default ResetPasswordContainer; 12 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/ForgotPasswordContainer.tsx: -------------------------------------------------------------------------------- 1 | import ForgotPassword from './ForgotPassword'; 2 | 3 | import useForgotPasswordContainer from './useForgotPassordContainer'; 4 | 5 | const ForgotPasswordContainer = () => { 6 | const { register, handleSubmit, errors } = useForgotPasswordContainer(); 7 | 8 | return ; 9 | }; 10 | 11 | export default ForgotPasswordContainer; 12 | -------------------------------------------------------------------------------- /src/components/UI/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | 4 | import { ILinkProps } from './types'; 5 | 6 | const Link: FC = ({ variant, to, children, className = '', ...props }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export default Link; 15 | -------------------------------------------------------------------------------- /src/components/UI/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { IButtonProps } from './types'; 4 | 5 | const Button: FC = ({ variant, className = '', children, isLoading, ...props }) => { 6 | return ( 7 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default Button; 17 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/components/SubscriptionCardFeauture.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .subscription-card-feature { 5 | @include border(top, var(--feature-item-border-color)); 6 | @include flex(row, center, flex-start); 7 | width: 100%; 8 | padding: 1rem 0; 9 | 10 | &:first-child { 11 | border-color: var(--black); 12 | } 13 | } 14 | .subscription-card-feature__value { 15 | margin-left: 0.5rem; 16 | } -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/useBaseLayoutContainer.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | import { selectAccessToken } from '../../../store/user/user.selectors'; 4 | 5 | import { IUseBaseLayoutContainerReturnData } from './types'; 6 | 7 | const useBaseLayoutContainer = (): IUseBaseLayoutContainerReturnData => { 8 | const accessToken = useSelector(selectAccessToken); 9 | 10 | return { 11 | accessToken, 12 | }; 13 | }; 14 | 15 | export default useBaseLayoutContainer; 16 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/VerifyEmail.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .verify-email { 5 | @include flex(column, center, flex-start); 6 | } 7 | .verify-email__title { 8 | font-size: 2rem; 9 | margin-bottom: 1rem; 10 | } 11 | .verify-email__details { 12 | @include flex(column, center, flex-start); 13 | } 14 | .verify-email__info { 15 | margin-bottom: 0.5rem; 16 | } 17 | .verify-email__link { 18 | color: var(--link); 19 | text-decoration: underline; 20 | } -------------------------------------------------------------------------------- /src/components/UI/Avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Icon } from 'components'; 4 | 5 | import { IAvatarProps } from './types'; 6 | 7 | import styles from './Avatar.module.scss'; 8 | 9 | const Avatar: FC = ({ src }) => { 10 | return ( 11 | 12 | {src ? : } 13 | 14 | ); 15 | }; 16 | 17 | export default Avatar; 18 | -------------------------------------------------------------------------------- /src/components/shared/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | // Components 4 | import { UserAstronaout, CheckMark } from './icons'; 5 | 6 | // Types 7 | import { IIconProps } from './types'; 8 | 9 | export const icons = { 10 | 'user-astronaut': UserAstronaout, 11 | 'check-mark': CheckMark, 12 | }; 13 | 14 | const Icon: FC = ({ name, ...props }) => { 15 | const IconComponent = icons[name]; 16 | 17 | return ; 18 | }; 19 | 20 | export default Icon; 21 | -------------------------------------------------------------------------------- /src/pages/user/Success/Success.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .success { 5 | padding: 1rem 0; 6 | } 7 | .success-content { 8 | @include flex(column, center, flex-start); 9 | } 10 | .success__title { 11 | font-size: 2rem; 12 | color: var(--primary); 13 | margin-bottom: 1rem; 14 | } 15 | .success__image-container { 16 | max-width: 25rem; 17 | width: 100%; 18 | margin-bottom: 1rem; 19 | } 20 | .success__image { 21 | width: 100%; 22 | display: block; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/schemas/forgotPasswordSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | // Constants 4 | import { requiredMessage, emailMessage, validationLength, maxLengthMessage } from '../../constants'; 5 | 6 | const forgotPasswordSchema = yup.object({ 7 | email: yup.string().email(emailMessage).max(validationLength.base, maxLengthMessage.base).required(requiredMessage), 8 | }); 9 | 10 | export type ForgotPasswordFormDataType = yup.InferType; 11 | 12 | export default forgotPasswordSchema; 13 | -------------------------------------------------------------------------------- /src/store/ui/ui.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | import { changeTheme } from './ui.actions'; 4 | 5 | import { IUIState } from './types'; 6 | 7 | const initialState: IUIState = { 8 | theme: 'light', 9 | }; 10 | 11 | const uiReducer = createReducer(initialState, (buider) => { 12 | buider 13 | .addCase(changeTheme, (state, action) => { 14 | state.theme = action.payload; 15 | }) 16 | .addDefaultCase((state) => state); 17 | }); 18 | 19 | export default uiReducer; 20 | -------------------------------------------------------------------------------- /src/pages/user/Home/Home.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .home { 5 | padding: 1rem 0; 6 | } 7 | .home-content { 8 | @include flex(column, center, flex-start); 9 | } 10 | .home__title { 11 | font-size: 2.5rem; 12 | margin-bottom: 2rem; 13 | color: var(--primary); 14 | } 15 | .home__subscription-cards { 16 | @include flex(row, stretch, space-between); 17 | width: 100%; 18 | gap: 0.5rem; 19 | flex-wrap: wrap; 20 | } 21 | .home__subscription-card { 22 | width: calc(25% - 0.5rem); 23 | } -------------------------------------------------------------------------------- /src/pages/user/Success/__tests__/Success.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import Success from '../Success'; 5 | 6 | describe('Success', () => { 7 | it('should contain text "Payment Successful"', function () { 8 | const { container } = render( 9 | 10 | 11 | 12 | ); 13 | 14 | const title = container.querySelector('h2'); 15 | expect(title?.textContent).toContain('Payment Successful'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/LoginSuggesting/LoginSuggesting.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'components'; 2 | 3 | import styles from './LogginSuggestions.module.scss'; 4 | 5 | const LoginSuggesting = () => { 6 | return ( 7 | 8 | Hey try to log in 9 | 10 | Log In 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default LoginSuggesting; 17 | -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/VerifyEmailContainer.tsx: -------------------------------------------------------------------------------- 1 | import VerifyEmail from './VerifyEmail'; 2 | 3 | import useVerifyEmailContainer from './userVerifyEmailContainer'; 4 | 5 | const VerifyEmailContainer = () => { 6 | const { isVerificationPassed, loading, error, showLogInLink } = useVerifyEmailContainer(); 7 | 8 | return ( 9 | 15 | ); 16 | }; 17 | 18 | export default VerifyEmailContainer; 19 | -------------------------------------------------------------------------------- /src/services/ActiveSubscriptionService.ts: -------------------------------------------------------------------------------- 1 | import Service from './Service'; 2 | 3 | import { request } from 'utils'; 4 | 5 | import { IResponseData } from './types'; 6 | 7 | class ActiveSubscriptionService extends Service { 8 | static createCheckoutSession(data: D) { 9 | return request>('POST', 'active-subscription/create-checkout-session', data); 10 | } 11 | 12 | static createActiveSubscription(data: D) { 13 | return request>('POST', 'active-subscription', data); 14 | } 15 | } 16 | 17 | export default ActiveSubscriptionService; 18 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: string; 3 | email: string; 4 | firstName: string; 5 | lastName: string; 6 | role: string; 7 | } 8 | 9 | export interface IUserSignInData { 10 | email: string; 11 | password: string; 12 | } 13 | 14 | export interface IUserSignUpData extends IUserSignInData { 15 | firstName: string; 16 | lastName: string; 17 | confirmPassword: string; 18 | } 19 | 20 | export interface IUserForgotPasswordData { 21 | email: string; 22 | } 23 | 24 | export interface IUserResetPasswordData { 25 | newPassword: string; 26 | confirmPassword: string; 27 | } 28 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import Service from './Service'; 2 | 3 | import { request } from 'utils'; 4 | 5 | import { IResponseData } from './types'; 6 | 7 | class UserService extends Service { 8 | static signUp(data: D) { 9 | return request>('POST', 'user/sign-up', data); 10 | } 11 | 12 | static verifyEmail(token: D) { 13 | return request>('POST', `user/verify-email/${token}`); 14 | } 15 | 16 | static signIn(data: D) { 17 | return request>('POST', 'user/sign-in', data); 18 | } 19 | } 20 | 21 | export default UserService; 22 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/__tests__/BaseLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import BaseLayout from '../BaseLayout'; 4 | 5 | let container: HTMLElement; 6 | 7 | describe('BaseLayout', () => { 8 | beforeEach(() => { 9 | container = render().container; 10 | }); 11 | 12 | it('should have 1 main tag inside', () => { 13 | expect(container.querySelectorAll('main').length).toBe(1); 14 | }); 15 | 16 | it('should have 1 header tag inside', () => { 17 | expect(container.querySelectorAll('header').length).toBe(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/pages/user/Success/useSuccessContainer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAppDispatch } from '../../../store'; 3 | 4 | import { useQuery } from 'hooks'; 5 | 6 | import { createActiveSubscription } from 'store/activeSubscription/activeSubscription.action'; 7 | 8 | const useSuccessContainer = () => { 9 | const dispatch = useAppDispatch(); 10 | const query = useQuery(); 11 | 12 | const planId = query.get('planId'); 13 | 14 | useEffect(() => { 15 | if (planId) { 16 | dispatch(createActiveSubscription({ planId })); 17 | } 18 | }, []); 19 | }; 20 | 21 | export default useSuccessContainer; 22 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .header { 5 | position: absolute; 6 | width: 100%; 7 | left: 0; 8 | top: 0; 9 | padding: 1rem 0; 10 | box-shadow: var(--box-shadow); 11 | } 12 | .header-content { 13 | @include flex(row, center, space-between); 14 | } 15 | .header__logo { 16 | @include flex(row, center, flex-start); 17 | } 18 | .header__logo-image-wrapper { 19 | width: 2rem; 20 | margin-right: 1rem; 21 | } 22 | .header__logo-image { 23 | width: 100%; 24 | display: block; 25 | } 26 | .header__title { 27 | font-size: 1.5rem; 28 | } -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/types.ts: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes } from 'react'; 2 | 3 | import { VariantType, IPlanItem } from 'types'; 4 | import { UseSubscriptionCardType } from './useSubscriptionCardContainer'; 5 | 6 | export interface ISubscriptionCardContainerProps extends HTMLAttributes { 7 | variant: VariantType; 8 | id: string; 9 | name: string; 10 | price: number; 11 | planItems: IPlanItem[]; 12 | } 13 | 14 | export interface ISubscriptionCardProps extends Omit { 15 | plansItemsContent: React.JSX.Element[]; 16 | handleActivateClick: () => void; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/BaseLayout.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | import { Header } from './components/Header'; 5 | import { LogginSuggesting } from './components/LoginSuggesting'; 6 | 7 | import { IBaseLayoutProps } from './types'; 8 | 9 | import styles from './BaseLayout.module.scss'; 10 | 11 | const BaseLayout: FC = ({ accessToken }) => { 12 | return ( 13 | <> 14 | 15 | {accessToken ? : } 16 | > 17 | ); 18 | }; 19 | 20 | export default BaseLayout; 21 | -------------------------------------------------------------------------------- /src/pages/user/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Container } from 'components'; 4 | 5 | import { IHomeProps } from './types'; 6 | 7 | import styles from './Home.module.scss'; 8 | 9 | const Home: FC = ({ plansListContent }) => { 10 | return ( 11 | 12 | 13 | 14 | Choose your subscription 15 | {plansListContent} 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /src/App/useAppContainer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAppSelector, useAppDispatch } from '../store'; 3 | 4 | import { changeTheme } from 'store/ui/ui.actions'; 5 | 6 | import { getPreferredTheme } from 'utils'; 7 | 8 | const useAppContainer = () => { 9 | const dispatch = useAppDispatch(); 10 | 11 | const { theme } = useAppSelector((state) => state.ui); 12 | 13 | useEffect(() => { 14 | const preferredTheme = getPreferredTheme(); 15 | 16 | if (theme !== preferredTheme) { 17 | dispatch(changeTheme(preferredTheme)); 18 | } 19 | }, []); 20 | 21 | return { 22 | theme, 23 | }; 24 | }; 25 | 26 | export default useAppContainer; 27 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/components/SubscriptionCardFeauture.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Icon } from 'components'; 4 | 5 | import { ISubscriptionCardFeatureProps } from './types'; 6 | 7 | import styles from './SubscriptionCardFeauture.module.scss'; 8 | 9 | const SubscriptionCardFeature: FC = ({ value }) => { 10 | return ( 11 | 12 | 13 | {value} 14 | 15 | ); 16 | }; 17 | 18 | export default SubscriptionCardFeature; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /src/store/user/user.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | 3 | import { RootState } from 'store/cofigureStore'; 4 | 5 | const userState = (state: RootState) => state.user; 6 | 7 | export const selectVerificationData = createSelector(userState, ({ verificationData }) => verificationData); 8 | export const selectLoading = createSelector(userState, ({ loading }) => loading); 9 | export const selectIsVerificationPassed = createSelector(userState, ({ isVerificationPassed }) => isVerificationPassed); 10 | export const selectError = createSelector(userState, ({ error }) => error); 11 | export const selectAccessToken = createSelector(userState, ({ accessToken }) => accessToken); 12 | -------------------------------------------------------------------------------- /src/utils/schemas/signInSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | // Constants 4 | import { 5 | requiredMessage, 6 | validationLength, 7 | minValidationLength, 8 | minLengthMessage, 9 | maxLengthMessage, 10 | } from '../../constants'; 11 | 12 | const signInSchema = yup.object({ 13 | email: yup.string().email().max(validationLength.base, maxLengthMessage.base).required(requiredMessage), 14 | password: yup 15 | .string() 16 | .min(minValidationLength.base, minLengthMessage.base) 17 | .max(validationLength.base, maxLengthMessage.base) 18 | .required(requiredMessage), 19 | }); 20 | 21 | export type SignInFormDataType = yup.InferType; 22 | 23 | export default signInSchema; 24 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/useSubscriptionCardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch } from '../../../../../store'; 2 | 3 | import { createCheckoutSession } from 'store/activeSubscription/activeSubscription.action'; 4 | 5 | import { IActiveSubscriptionData } from 'types'; 6 | 7 | const useSubscriptionCardContainer = () => { 8 | const dispatch = useAppDispatch(); 9 | 10 | const handleActivateClick = (data: IActiveSubscriptionData) => { 11 | dispatch(createCheckoutSession(data)); 12 | }; 13 | 14 | return { 15 | handleActivateClick, 16 | }; 17 | }; 18 | 19 | export type UseSubscriptionCardType = ReturnType; 20 | 21 | export default useSubscriptionCardContainer; 22 | -------------------------------------------------------------------------------- /src/utils/schemas/resetPasswordSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | // Constants 4 | import { 5 | requiredMessage, 6 | passwordsMessage, 7 | validationLength, 8 | minValidationLength, 9 | minLengthMessage, 10 | maxLengthMessage, 11 | } from '../../constants'; 12 | 13 | const resetPasswordSchema = yup.object({ 14 | newPassword: yup 15 | .string() 16 | .min(minValidationLength.base, minLengthMessage.base) 17 | .max(validationLength.base, maxLengthMessage.base) 18 | .required(requiredMessage), 19 | confirmPassword: yup.string().oneOf([yup.ref('newPassword')], passwordsMessage), 20 | }); 21 | 22 | export type ResetPasswordFormDataType = yup.InferType; 23 | 24 | export default resetPasswordSchema; 25 | -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/__tests__/VerifyEmail.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import VerifyEmail from '../VerifyEmail'; 5 | 6 | describe('VerifyEmail', () => { 7 | it('should work correctly for passed verification', () => { 8 | const { getByText, getByRole } = render( 9 | 10 | 11 | 12 | ); 13 | 14 | const verifiedText = getByText(/Email verified successfully\./i); 15 | const logInLink = getByRole('link'); 16 | 17 | expect(verifiedText).toBeTruthy(); 18 | expect(logInLink).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/useForgotPassordContainer.ts: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import { yupResolver } from '@hookform/resolvers/yup'; 3 | 4 | import { forgotPasswordSchema } from 'utils'; 5 | 6 | import { IUserForgotPasswordData } from 'types'; 7 | 8 | const useForgotPasswordContainer = () => { 9 | const { 10 | register, 11 | handleSubmit, 12 | formState: { errors }, 13 | } = useForm({ 14 | resolver: yupResolver(forgotPasswordSchema), 15 | }); 16 | 17 | return { 18 | register, 19 | handleSubmit, 20 | errors, 21 | }; 22 | }; 23 | 24 | export type UseForgotPasswordContainerType = ReturnType; 25 | 26 | export default useForgotPasswordContainer; 27 | -------------------------------------------------------------------------------- /src/store/plan/plan.action.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import PlanService from 'services/PlanService'; 4 | 5 | import { GET_PLANS } from './plan.actionTypes'; 6 | 7 | import { GetPlansReturnDataType } from './types'; 8 | 9 | import { globalMessages } from 'constants/messages'; 10 | 11 | export const getPlans = createAsyncThunk(GET_PLANS, async () => { 12 | try { 13 | const response = await PlanService.getPlans(); 14 | 15 | if (!response.data?.success) { 16 | throw new Error(response.data.message || globalMessages.smthWentWrong); 17 | } 18 | 19 | return response.data.data; 20 | } catch (error: any) { 21 | console.log(error); 22 | throw error; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/UI/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef } from 'react'; 2 | 3 | import { IInputProps } from './types'; 4 | 5 | const Input: FC = forwardRef( 6 | ({ variant = 'primary', error, className = '', wrapperClassName = '', ...props }, ref = null) => { 7 | return ( 8 | 9 | 17 | {error && {error}} 18 | 19 | ); 20 | } 21 | ); 22 | 23 | export default Input; 24 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/AuthContainer.tsx: -------------------------------------------------------------------------------- 1 | import Auth from './Auth'; 2 | 3 | import useAuthContainer from './useAuthContainer'; 4 | 5 | const AuthContainer = () => { 6 | const { 7 | isSignUp, 8 | handleFormSubmit, 9 | handleToggleIsSignUp, 10 | register, 11 | handleSubmit, 12 | errors, 13 | verificationData, 14 | loading, 15 | } = useAuthContainer(); 16 | 17 | return ( 18 | 28 | ); 29 | }; 30 | 31 | export default AuthContainer; 32 | -------------------------------------------------------------------------------- /src/components/shared/Icon/icons/CheckMark.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | // Types 4 | import { IIconComponentProps } from '../types'; 5 | 6 | const CheckMark: FC = ({ color = 'currentColor', width = 20, height = 20, ...props }) => { 7 | return ( 8 | 17 | 22 | 23 | ); 24 | }; 25 | 26 | export default CheckMark; 27 | -------------------------------------------------------------------------------- /src/pages/user/Success/Success.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Link } from 'components'; 2 | 3 | import styles from './Success.module.scss'; 4 | 5 | const Success = () => { 6 | return ( 7 | 8 | 9 | 10 | Payment Successful 11 | 12 | 13 | 14 | 15 | Go to home 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default Success; 24 | -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/useResetPasswordContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import { yupResolver } from '@hookform/resolvers/yup'; 3 | 4 | import { resetPasswordSchema } from 'utils'; 5 | 6 | import { IUserResetPasswordData } from 'types'; 7 | 8 | const useResetPasswordContainer = () => { 9 | const { 10 | register, 11 | handleSubmit, 12 | formState: { errors }, 13 | } = useForm({ 14 | // eslint-disable-next-line 15 | // @ts-ignore 16 | resolver: yupResolver(resetPasswordSchema), 17 | }); 18 | 19 | return { 20 | register, 21 | handleSubmit, 22 | errors, 23 | }; 24 | }; 25 | 26 | export type UseResetPasswordContainerType = ReturnType; 27 | 28 | export default useResetPasswordContainer; 29 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-demo.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Demo 2 | run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | init: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [ 18.x ] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Starting Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: install modules 21 | run: yarn install 22 | # - name: build project 23 | # run: yarn run build 24 | - name: lint code 25 | run: yarn lint 26 | - name: run tests 27 | run: yarn test 28 | -------------------------------------------------------------------------------- /src/assets/styles/components/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader-wrapper { 2 | width: 40px; 3 | height: 40px; 4 | display: inline-block; 5 | color: var(--primary); 6 | animation: rotate 1.4s linear infinite; 7 | } 8 | .loader { 9 | display: block; 10 | color: currentColor; 11 | } 12 | .loader-circle { 13 | stroke: currentColor; 14 | stroke-dasharray: 80px,200px; 15 | stroke-dashoffset: 0; 16 | animation: rotate-circle 1.4s ease-in-out infinite; 17 | } 18 | 19 | @keyframes rotate { 20 | 0% { 21 | transform: rotate(0deg); 22 | } 23 | 100% { 24 | transform: rotate(360deg); 25 | } 26 | } 27 | 28 | @keyframes rotate-circle { 29 | 0% { 30 | stroke-dasharray: 1px,200px; 31 | stroke-dashoffset: 0; 32 | } 33 | 50% { 34 | stroke-dasharray: 100px,200px; 35 | stroke-dashoffset: -15px; 36 | } 37 | 100% { 38 | stroke-dasharray: 100px,200px; 39 | stroke-dashoffset: -125px; 40 | } 41 | } -------------------------------------------------------------------------------- /src/store/plan/plan.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | import { getPlans } from './plan.action'; 4 | 5 | import { IPlanState } from './types'; 6 | 7 | const initialState: IPlanState = { 8 | list: [], 9 | loading: false, 10 | error: null, 11 | }; 12 | 13 | const planReducer = createReducer(initialState, (builder) => { 14 | builder 15 | .addCase(getPlans.fulfilled, (state, action) => { 16 | state.list = action.payload; 17 | state.loading = false; 18 | state.error = null; 19 | }) 20 | .addCase(getPlans.pending, (state) => { 21 | state.loading = true; 22 | state.error = null; 23 | }) 24 | .addCase(getPlans.rejected, (state, action) => { 25 | state.error = action.error?.message as string; 26 | state.loading = false; 27 | }) 28 | 29 | .addDefaultCase((state) => state); 30 | }); 31 | 32 | export default planReducer; 33 | -------------------------------------------------------------------------------- /src/store/user/types.ts: -------------------------------------------------------------------------------- 1 | import { IUserSignInData, IUserSignUpData, AccessTokenType, IUser } from 'types'; 2 | 3 | interface IVerificationData { 4 | email: string; 5 | } 6 | 7 | export interface IUserState { 8 | userData: IUser | null; 9 | accessToken: AccessTokenType; 10 | verificationData: IVerificationData | null; 11 | isVerificationPassed: boolean; 12 | error: null | string; 13 | loading: boolean; 14 | } 15 | 16 | // signUp action 17 | export type ISignUpPayloadData = IUserSignUpData; 18 | 19 | export interface ISignUpReturnData { 20 | email: string; 21 | } 22 | 23 | // verifyEmail action 24 | export interface IVerifyEmailPayloadData { 25 | token: string; 26 | } 27 | 28 | export interface IVerifyEmailReturnData { 29 | token: string; 30 | isEmailVerified: boolean; 31 | } 32 | 33 | // signIn action 34 | export type ISignInPayloadData = IUserSignInData; 35 | 36 | export interface ISignInReturnData { 37 | accessToken: string; 38 | userData: IUser; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { Method, AxiosResponse } from 'axios'; 2 | import store from 'store'; 3 | 4 | const axiosApi = axios.create({ 5 | baseURL: process.env.REACT_APP_API_URL, 6 | }); 7 | 8 | axiosApi.interceptors.request.use((req) => { 9 | const accessToken = store.get('accessToken'); 10 | 11 | if (req && req.headers && accessToken) { 12 | req.headers.Authorization = `Bearer ${accessToken}`; 13 | } 14 | 15 | return req; 16 | }); 17 | 18 | const request = >( 19 | method: Method, 20 | url: string, 21 | data?: D, 22 | params?: any 23 | ): Promise => { 24 | return axiosApi 25 | .request({ 26 | method, 27 | url, 28 | data, 29 | params, 30 | }) 31 | .then((response): R => { 32 | return response; 33 | }) 34 | .catch((error) => { 35 | console.log(method, url, error.message); 36 | return error.response; 37 | }); 38 | }; 39 | 40 | export default request; 41 | -------------------------------------------------------------------------------- /src/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | 4 | import { Home, Success, Auth, ForgotPassword, ResetPassword, VerifyEmail } from 'pages'; 5 | 6 | import { BaseLayout, AuthLayout } from 'components'; 7 | 8 | import { IAppProps } from './types'; 9 | 10 | const App: FC = ({ theme }) => { 11 | return ( 12 | 13 | 14 | }> 15 | } /> 16 | } /> 17 | 18 | }> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/VerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { Loader } from 'components'; 5 | 6 | import { IVerifyEmailProps } from './types'; 7 | 8 | import styles from './VerifyEmail.module.scss'; 9 | 10 | const VerifyEmail: FC = ({ isVerificationPassed, loading, showLogInLink }) => { 11 | return ( 12 | 13 | Verify Email 14 | {loading ? ( 15 | 16 | ) : ( 17 | 18 | {isVerificationPassed && Email verified successfully.} 19 | {showLogInLink && ( 20 | 21 | Log in 22 | 23 | )} 24 | 25 | )} 26 | 27 | ); 28 | }; 29 | 30 | export default VerifyEmail; 31 | -------------------------------------------------------------------------------- /src/utils/schemas/signUpSchema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | // Constants 4 | import { 5 | requiredMessage, 6 | emailMessage, 7 | passwordsMessage, 8 | validationLength, 9 | minValidationLength, 10 | minLengthMessage, 11 | maxLengthMessage, 12 | } from '../../constants'; 13 | 14 | const signUpSchema = yup.object({ 15 | firstName: yup.string().max(validationLength.base, maxLengthMessage.base).required(requiredMessage), 16 | lastName: yup.string().max(validationLength.base, maxLengthMessage.base).required(requiredMessage), 17 | email: yup.string().email(emailMessage).max(validationLength.base, maxLengthMessage.base).required(requiredMessage), 18 | password: yup 19 | .string() 20 | .min(minValidationLength.base, minLengthMessage.base) 21 | .max(validationLength.base, maxLengthMessage.base) 22 | .required(requiredMessage), 23 | confirmPassword: yup.string().oneOf([yup.ref('password')], passwordsMessage), 24 | }); 25 | 26 | export type SignUpFormDataType = yup.InferType; 27 | 28 | export default signUpSchema; 29 | -------------------------------------------------------------------------------- /src/assets/styles/abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin flex($direction: row, $align: stretch, $justify: flex-start) { 2 | display: flex; 3 | flex-direction: $direction; 4 | align-items: $align; 5 | justify-content: $justify; 6 | } 7 | 8 | @mixin border($side: all, $color: var(--border-color), $width: 0.0625rem, $type: solid) { 9 | @if $side == all { 10 | border: $type $width $color; 11 | } @else if $side == horizontal { 12 | border-left: $type $width $color; 13 | border-right: $type $width $color; 14 | } @else if $side == vertical { 15 | border-top: $type $width $color; 16 | border-bottom: $type $width $color; 17 | } @else if $side == left { 18 | border-left: $type $width $color; 19 | } @else if $side == right { 20 | border-right: $type $width $color; 21 | } @else if $side == top { 22 | border-top: $type $width $color; 23 | } @else if $side == bottom { 24 | border-bottom: $type $width $color; 25 | } @else { 26 | border: $type $width $color; 27 | } 28 | } 29 | 30 | @mixin focus-ring($color: $primary-light, $blur: 0) { 31 | box-shadow: 0 0 $blur 0.125rem $color; 32 | } -------------------------------------------------------------------------------- /src/constants/validation/index.ts: -------------------------------------------------------------------------------- 1 | // Typoes 2 | import { IValidationLength, IMinValidationLength } from './types'; 3 | 4 | export const requiredMessage = 'Required'; 5 | export const emailMessage = 'Invalid email address'; 6 | export const passwordsMessage = "Passwords don't match"; 7 | 8 | export const validationLength: IValidationLength = { 9 | short: 64, 10 | base: 128, 11 | long: 256, 12 | } as const; 13 | 14 | export const minValidationLength: IMinValidationLength = { 15 | base: 8, 16 | } as const; 17 | 18 | const generateMaxLengthMessage = (length: keyof IValidationLength) => { 19 | return `Text is too long (maximum is ${validationLength[length]})`; 20 | }; 21 | 22 | const generateMinLengthMessage = (length: keyof IMinValidationLength) => { 23 | return `At least ${minValidationLength[length]} characters needed`; 24 | }; 25 | 26 | export const maxLengthMessage = { 27 | short: generateMaxLengthMessage('short'), 28 | base: generateMaxLengthMessage('base'), 29 | long: generateMaxLengthMessage('long'), 30 | } as const; 31 | 32 | export const minLengthMessage = { 33 | base: generateMinLengthMessage('base'), 34 | } as const; 35 | -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/ResetPassword.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .reset-form { 5 | @include flex(column, center, flex-start); 6 | } 7 | .reset-form__title { 8 | font-size: 2rem; 9 | margin-bottom: 1rem; 10 | } 11 | .reset-form__inputs { 12 | width: 100%; 13 | margin-bottom: 1rem; 14 | } 15 | .reset-form__form-group { 16 | @include flex(column, flex-start, flex-start); 17 | width: 100%; 18 | 19 | &:not(:last-child) { 20 | margin-bottom: 0.75rem; 21 | } 22 | } 23 | .reset-form__label { 24 | margin-bottom: 0.25rem; 25 | } 26 | .reset-form__controls { 27 | width: 100%; 28 | @include flex(column, flex-start, flex-start); 29 | } 30 | .reset-form__controls-main { 31 | @include flex(row, center, flex-start); 32 | } 33 | .reset-form__controls-btn { 34 | margin-right: 0.5rem; 35 | } 36 | .reset-form__controls-change-mode { 37 | color: var(--link); 38 | text-decoration: underline; 39 | 40 | &:hover { 41 | cursor: pointer; 42 | } 43 | } 44 | .reset-form__controls-forgot { 45 | color: var(--link); 46 | text-decoration: underline; 47 | margin-top: 0.5rem; 48 | } -------------------------------------------------------------------------------- /src/components/layouts/BaseLayout/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Container, Avatar, Button } from 'components'; 4 | 5 | import { IHeaderProps } from './types'; 6 | 7 | import styles from './Header.module.scss'; 8 | 9 | const Header: FC = ({ accessToken }) => { 10 | return ( 11 | 12 | 13 | 14 | {/*TODO: explore why Link is not working*/} 15 | 16 | 17 | 18 | 19 | Checkinator 20 | 21 | 22 | {accessToken ? : Sign in} 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/ForgotPassword.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .forgot-form { 5 | @include flex(column, center, flex-start); 6 | } 7 | .forgot-form__title { 8 | font-size: 2rem; 9 | margin-bottom: 1rem; 10 | } 11 | .forgot-form__inputs { 12 | width: 100%; 13 | margin-bottom: 1rem; 14 | } 15 | .forgot-form__form-group { 16 | @include flex(column, flex-start, flex-start); 17 | width: 100%; 18 | 19 | &:not(:last-child) { 20 | margin-bottom: 0.75rem; 21 | } 22 | } 23 | .forgot-form__label { 24 | margin-bottom: 0.25rem; 25 | } 26 | .forgot-form__controls { 27 | width: 100%; 28 | @include flex(column, flex-start, flex-start); 29 | } 30 | .forgot-form__controls-main { 31 | @include flex(row, center, flex-start); 32 | } 33 | .forgot-form__controls-btn { 34 | margin-right: 0.5rem; 35 | } 36 | .forgot-form__controls-change-mode { 37 | color: var(--link); 38 | text-decoration: underline; 39 | 40 | &:hover { 41 | cursor: pointer; 42 | } 43 | } 44 | .forgot-form__controls-forgot { 45 | color: var(--link); 46 | text-decoration: underline; 47 | margin-top: 0.5rem; 48 | } -------------------------------------------------------------------------------- /src/assets/styles/components/_input.scss: -------------------------------------------------------------------------------- 1 | .input-wrapper { 2 | position: relative; 3 | width: 100%; 4 | } 5 | .input { 6 | outline: none; 7 | display: block; 8 | width: 100%; 9 | padding: 0.5rem; 10 | border-radius: 0.25rem; 11 | font-size: 1rem; 12 | background-color: var(--bf-color); 13 | color: var(--primary-text-color); 14 | @include border(all, var(--border-color), 0.06rem); 15 | 16 | transition: box-shadow .3s ease-in-out, border-color .3s ease-in-out; 17 | 18 | &--primary { 19 | 20 | &:focus { 21 | border-color: var(--primary); 22 | @include focus-ring(var(--primary)); 23 | } 24 | } 25 | 26 | &--secondary { 27 | 28 | &:focus { 29 | border-color: var(--secondary); 30 | @include focus-ring(var(--secondary)); 31 | } 32 | } 33 | 34 | // &--error { 35 | // border-color: $danger !important; 36 | 37 | // &:focus { 38 | // @include focus-ring($danger); 39 | // } 40 | // } 41 | } 42 | .input__error-message { 43 | position: absolute; 44 | left: 0; 45 | top: 100%; 46 | color: red; // TODO: create danger variable and change with that 47 | font-size: 0.625rem; 48 | } -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/SubscriptionCardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import SubscriptionCard from './SubscriptionCard'; 4 | import { SubscriptionCardFeature } from './components'; 5 | 6 | import useSubscriptionCardContainer from './useSubscriptionCardContainer'; 7 | 8 | import { ISubscriptionCardContainerProps } from './types'; 9 | 10 | const SubscriptionCardContainer: FC = ({ variant, planItems, ...props }) => { 11 | const { handleActivateClick } = useSubscriptionCardContainer(); 12 | 13 | const planItemsContent = planItems.map((planItem) => { 14 | return ; 15 | }); 16 | 17 | const handleClick = () => { 18 | handleActivateClick({ 19 | id: props.id, 20 | name: props.name, 21 | price: props.price, 22 | }); 23 | }; 24 | 25 | return ( 26 | 32 | ); 33 | }; 34 | 35 | export default SubscriptionCardContainer; 36 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import App from 'App'; 7 | 8 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 9 | import reportWebVitals from './reportWebVitals'; 10 | 11 | import store from 'store/cofigureStore'; 12 | 13 | import './assets/styles/index.scss'; 14 | 15 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 16 | root.render( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | // If you want your app to work offline and load faster, you can change 25 | // unregister() to register() below. Note this comes with some pitfalls. 26 | // Learn more about service workers: https://cra.link/PWA 27 | serviceWorkerRegistration.unregister(); 28 | 29 | // If you want to start measuring performance in your app, pass a function 30 | // to log results (for example: reportWebVitals(console.log)) 31 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 32 | reportWebVitals(); 33 | -------------------------------------------------------------------------------- /src/pages/guest/VerifyEmail/userVerifyEmailContainer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { useAppDispatch, useAppSelector } from '../../../store'; 4 | 5 | import { verifyEmail } from 'store/user/user.action'; 6 | import { selectIsVerificationPassed, selectLoading, selectError } from 'store/user/user.selectors'; 7 | 8 | const useVerifyEmailContainer = () => { 9 | const dispatch = useAppDispatch(); 10 | 11 | const isVerificationPassed = useAppSelector(selectIsVerificationPassed); 12 | const loading = useAppSelector(selectLoading); 13 | const error = useAppSelector(selectError); 14 | 15 | const { token } = useParams(); 16 | 17 | const showLogInLink = isVerificationPassed || error; 18 | 19 | useEffect(() => { 20 | if (token) { 21 | dispatch(verifyEmail({ token })); 22 | } else { 23 | // Some message 24 | } 25 | }, []); 26 | 27 | return { 28 | isVerificationPassed, 29 | loading, 30 | error, 31 | showLogInLink, 32 | }; 33 | }; 34 | 35 | export type UseVerifyEmailContainerType = ReturnType; 36 | 37 | export default useVerifyEmailContainer; 38 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/Auth.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .auth-form { 5 | @include flex(column, center, flex-start); 6 | } 7 | .auth-form__title { 8 | font-size: 2rem; 9 | margin-bottom: 1rem; 10 | } 11 | .auth-form__inputs { 12 | width: 100%; 13 | margin-bottom: 1rem; 14 | } 15 | .auth-form__form-group { 16 | @include flex(column, flex-start, flex-start); 17 | width: 100%; 18 | 19 | &:not(:last-child) { 20 | margin-bottom: 0.75rem; 21 | } 22 | } 23 | .auth-form__label { 24 | margin-bottom: 0.25rem; 25 | } 26 | .auth-form__verification-data { 27 | margin-bottom: 1rem; 28 | text-align: center; 29 | } 30 | .auth-form__controls { 31 | width: 100%; 32 | @include flex(column, flex-start, flex-start); 33 | } 34 | .auth-form__controls-main { 35 | @include flex(row, center, flex-start); 36 | } 37 | .auth-form__controls-btn { 38 | margin-right: 0.5rem; 39 | } 40 | .auth-form__controls-change-mode { 41 | color: var(--link); 42 | text-decoration: underline; 43 | 44 | &:hover { 45 | cursor: pointer; 46 | } 47 | } 48 | .auth-form__controls-forgot { 49 | color: var(--link); 50 | text-decoration: underline; 51 | margin-top: 0.5rem; 52 | } -------------------------------------------------------------------------------- /src/components/UI/Link/__tests/Link.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import Link from '../Link'; 5 | 6 | describe('Link Component', () => { 7 | it('should render link correctly', () => { 8 | const { container } = render( 9 | 10 | Link 11 | 12 | ); 13 | const linkElement = container.querySelector('a'); 14 | expect(linkElement).toBeInTheDocument(); 15 | }); 16 | 17 | it('should apply className correctly', () => { 18 | const { container } = render( 19 | 20 | 21 | 22 | ); 23 | const linkElement = container.querySelector('.custom-link'); 24 | expect(linkElement).toBeInTheDocument(); 25 | }); 26 | 27 | it('should apply variant correctly', () => { 28 | const { container } = render( 29 | 30 | 31 | 32 | ); 33 | const linkElement = container.querySelector('.base-button--primary'); 34 | expect(linkElement).toBeInTheDocument(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/pages/guest/ForgotPassword/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Button, Input, Link } from 'components'; 4 | 5 | import { IForgotPasswordProps } from './types'; 6 | 7 | import styles from './ForgotPassword.module.scss'; 8 | 9 | const ForgotPassword: FC = () => { 10 | return ( 11 | 12 | Forgot Password 13 | 14 | 15 | Email 16 | 17 | 18 | 19 | 20 | 21 | 22 | Send Email 23 | 24 | 25 | 26 | Log in 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default ForgotPassword; 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | commonjs: true, 5 | node: true, 6 | browser: true, 7 | es6: true, 8 | jest: true, 9 | }, 10 | plugins: ['prettier', 'react'], 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'eslint:recommended', 14 | 'plugin:react/recommended', 15 | 'plugin:prettier/recommended', 16 | 'prettier', 17 | ], 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | ecmaVersion: 2021, 24 | sourceType: 'module', 25 | parser: '@babel/eslint-parser', 26 | }, 27 | ignorePatterns: ['node_modules/'], 28 | rules: { 29 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 30 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 31 | 'prettier/prettier': ['error'], 32 | 'react/prop-types': 'off', 33 | 'no-unused-vars': 'off', 34 | 'react-hooks/exhaustive-deps': 'off', 35 | 'no-extra-boolean-cast': 'off', 36 | 'react/jsx-no-target-blank': 'off', 37 | 'react/no-unescaped-entities': 'off', 38 | 'react/react-in-jsx-scope': 'off', 39 | 'react/display-name': 'off', 40 | }, 41 | globals: { 42 | ServiceWorkerGlobalScope: true, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/pages/user/Home/useHomeContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAppDispatch, useAppSelector } from '../../../store'; 3 | 4 | import { SubscriptionCard } from './components/SubscriptionCard'; 5 | 6 | import { getPlans } from 'store/plan/plan.action'; 7 | 8 | import { selectPlansList } from 'store/plan/plan.selectors'; 9 | 10 | import { VariantType } from 'types'; 11 | 12 | import styles from './Home.module.scss'; 13 | 14 | const useHomeContainer = () => { 15 | const dispatch = useAppDispatch(); 16 | 17 | const plansList = useAppSelector(selectPlansList); 18 | 19 | const plansListContent = plansList.map((planItem) => { 20 | return ( 21 | 31 | ); 32 | }); 33 | 34 | useEffect(() => { 35 | dispatch(getPlans()); 36 | }, []); 37 | 38 | return { 39 | plansListContent, 40 | }; 41 | }; 42 | 43 | export type UseHomeContainerType = ReturnType; 44 | 45 | export default useHomeContainer; 46 | -------------------------------------------------------------------------------- /src/components/UI/Button/__tests/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from '@testing-library/react'; 2 | 3 | import Button from '../Button'; 4 | 5 | describe('Button Component', () => { 6 | it('should render children correctly', () => { 7 | const { getByText } = render(Hello World); 8 | const buttonElement = getByText('Hello World'); 9 | expect(buttonElement).toBeInTheDocument(); 10 | }); 11 | 12 | it('should apply className correctly', () => { 13 | const { container } = render(Hello World); 14 | const buttonElement = container.querySelector('.base-button.some-class'); 15 | expect(buttonElement).toBeInTheDocument(); 16 | }); 17 | 18 | it('should handle click events', () => { 19 | const handleClick = jest.fn(); 20 | const { getByText } = render(Hello World); 21 | const buttonElement = getByText('Hello World'); 22 | 23 | fireEvent.click(buttonElement); 24 | 25 | expect(handleClick).toHaveBeenCalled(); 26 | }); 27 | 28 | it('displays correct variant', () => { 29 | const { getByText } = render(Primary Button); 30 | const buttonElement = getByText('Primary Button'); 31 | expect(buttonElement).toHaveClass('base-button--primary'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/pages/guest/ResetPassword/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Button, Input, Link } from 'components'; 3 | 4 | import { IResetPasswordProps } from './types'; 5 | 6 | import styles from './ResetPassword.module.scss'; 7 | 8 | const ResetPassword: FC = () => { 9 | return ( 10 | 11 | Reset Password 12 | 13 | 14 | New Password 15 | 16 | 17 | 18 | Repeat Password 19 | 20 | 21 | 22 | 23 | 24 | 25 | Reset 26 | 27 | 28 | 29 | Log in 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default ResetPassword; 37 | -------------------------------------------------------------------------------- /src/assets/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | a { 50 | text-decoration: none; 51 | color: inherit; 52 | } 53 | *, *::before, *::after { 54 | box-sizing: border-box; 55 | } -------------------------------------------------------------------------------- /src/components/UI/Input/__tests__/Input.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import Input from '../Input'; 4 | 5 | describe('Input Component', () => { 6 | it('should render children correctly', () => { 7 | const { container } = render(); 8 | const inputElement = container.querySelector('input'); 9 | expect(inputElement).toBeInTheDocument(); 10 | }); 11 | 12 | it('should apply className correctly', () => { 13 | const { container } = render(); 14 | const inputElement = container.querySelector('.input.custom-input'); 15 | expect(inputElement).toBeInTheDocument(); 16 | }); 17 | 18 | it('should apply wrapperClassName correctly', () => { 19 | const { container } = render(); 20 | const inputElement = container.querySelector('.input-wrapper.wrapper'); 21 | expect(inputElement).toBeInTheDocument(); 22 | }); 23 | 24 | it('should display error message', () => { 25 | const { getByText } = render(); 26 | const errorMessage = getByText('Invalid input'); 27 | expect(errorMessage).toBeInTheDocument(); 28 | }); 29 | 30 | it('should display variant correctly', () => { 31 | const { container } = render(); 32 | const inputElement = container.querySelector('.input--secondary'); 33 | expect(inputElement).toBeInTheDocument(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/shared/Icon/icons/UserAstronaut.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | // Types 4 | import { IIconComponentProps } from '../types'; 5 | 6 | const UserAstronaut: FC = ({ color = 'currentColor', width = 24, height = 24, ...props }) => { 7 | return ( 8 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default UserAstronaut; 23 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/__tests__/SubscriptionCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import SubscriptionCard from '../SubscriptionCard'; 4 | import { SubscriptionCardFeature } from '../components'; 5 | 6 | import { mockPlansData } from 'tests/constants'; 7 | 8 | const planItemsContent = mockPlansData[0].planItems.map((planItem) => { 9 | return ; 10 | }); 11 | 12 | describe('SubscriptionCard', () => { 13 | it('should contain title including word "Plan"', () => { 14 | const { container } = render( 15 | 23 | ); 24 | 25 | const title = container.querySelector('h3'); 26 | 27 | expect(title).not.toBeNull(); 28 | expect(title?.textContent).toContain('Plan'); 29 | }); 30 | 31 | it('should have list of features containing at least 1 item', () => { 32 | const { container } = render( 33 | 41 | ); 42 | 43 | const list = container.querySelector('ul'); 44 | 45 | expect(list?.children.length).toBeGreaterThanOrEqual(1); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/assets/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // Main colors 3 | --primary-light: #51aa51; 4 | --primary: #307a30; 5 | --primary-dark: #2a6a2a; 6 | 7 | --secondary-light: #ffe436; 8 | --secondary: #ffcc00; 9 | --secondary-dark: #ffbb00; 10 | 11 | --link: #6868ef; 12 | 13 | --primary-text-color: #010b01; 14 | 15 | // Abstract color 16 | --white: #ffffff; 17 | --black: #000000; 18 | --light-gray: #cdcdcd; 19 | 20 | // Sense colors 21 | --bg-color: #ffffff; 22 | --border-color: #121212; 23 | --feature-item-border-color: #5e636b; 24 | --button-white: #ffffff; 25 | --button-black: #000000; 26 | 27 | // Inner colors 28 | --subscription-purple: #818cf8; 29 | --subscription-purple-light: #b3b9ff; 30 | --subscription-purple-dark: #5e63ff; 31 | 32 | --subscription-blue: #3b82f6; 33 | --subscription-blue-light: #93c5fd; 34 | --subscription-blue-dark: #0f4cfc; 35 | 36 | // Basic styles 37 | --box-shadow: 0 0 0.5rem -0.25rem #121212; 38 | --box-shadow-primary: 0 0 0.1rem 0.1rem var(--primary); 39 | --box-shadow-secondary: 0 0 0.1rem 0.1rem var(--secondary); 40 | --box-shadow-subscription-purple: 0 0 0.1rem 0.1rem var(--subscription-purple); 41 | --box-shadow-subscription-blue: 0 0 0.1rem 0.1rem var(--subscription-blue); 42 | } 43 | 44 | .App.dark { 45 | // Main colors 46 | --primary-text-color: #e4e7e4; 47 | 48 | // Abstract color 49 | --white: #000000; 50 | --black: #ffffff; 51 | 52 | // Sense color 53 | --bg-color: #1f1f1f; 54 | --border-color: #ffffff; 55 | --feature-item-border-color: #1e293b; 56 | 57 | // Basic styles 58 | --box-shadow: 0 0 0.5rem -0.25rem #eeeeee; 59 | } -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/SubscriptionCard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Button } from 'components'; 4 | import { SubscriptionCardFeature } from './components'; 5 | 6 | import { ISubscriptionCardProps } from './types'; 7 | 8 | import styles from './SubscriptionCard.module.scss'; 9 | 10 | const SubscriptionCard: FC = ({ 11 | variant, 12 | className = '', 13 | name, 14 | id, 15 | price, 16 | plansItemsContent, 17 | handleActivateClick, 18 | ...props 19 | }) => { 20 | return ( 21 | 25 | 26 | 27 | {name} Plan 28 | 29 | 30 | 31 | {price}$ 32 | / month 33 | 34 | Billed monthly 35 | 36 | Activate 37 | 38 | 39 | 40 | {plansItemsContent} 41 | 42 | ); 43 | }; 44 | 45 | export default SubscriptionCard; 46 | -------------------------------------------------------------------------------- /src/pages/user/Home/__tests__/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, waitFor } from '@testing-library/react'; 2 | 3 | import Home from '../Home'; 4 | import { SubscriptionCardFeature } from '../components/SubscriptionCard/components'; 5 | import SubscriptionCard from '../components/SubscriptionCard/SubscriptionCard'; 6 | 7 | import { mockPlansData } from 'tests/constants'; 8 | 9 | import { VariantType } from 'types'; 10 | 11 | let container: HTMLElement; 12 | let component: any; 13 | 14 | describe('Home', () => { 15 | beforeEach(() => { 16 | const planItemsContent = mockPlansData[0].planItems.map((planItem) => { 17 | return ; 18 | }); 19 | const plansListContent = mockPlansData.map((planItem) => { 20 | return ( 21 | 32 | ); 33 | }); 34 | ({ container, ...component } = render()); 35 | }); 36 | 37 | it('should have 1 h2 tag inside', () => { 38 | expect(container.querySelectorAll('h2').length).toBe(1); 39 | }); 40 | 41 | it('should have 4 subscription cards', async () => { 42 | const { getAllByTestId } = component; 43 | await waitFor(() => { 44 | const subscriptionCards = getAllByTestId('subscription-card'); 45 | expect(subscriptionCards.length).toBe(4); 46 | }); 47 | }); 48 | 49 | // TODO make tests advanced by checking the content inside 50 | }); 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Checkinator 25 | 26 | 27 | You need to enable JavaScript to run this app. 28 | 29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/store/activeSubscription/activeSubscription.action.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import ActiveSubscriptionService from 'services/ActiveSubscriptionService'; 4 | 5 | import { getStripe } from 'lib/stripe'; 6 | 7 | import { CREATE_CHECKOUT_SESSION, CREATE_ACTIVE_SUBSCRIPTION } from './activeSubscription.actionTypes'; 8 | 9 | import { globalMessages } from '../../constants'; 10 | 11 | import { ICreateCheckoutSessionPayloadData, ICreateActiveSubscriptionPayloadData } from './types'; 12 | 13 | export const createCheckoutSession = createAsyncThunk( 14 | CREATE_CHECKOUT_SESSION, 15 | async (data) => { 16 | try { 17 | const response = await ActiveSubscriptionService.createCheckoutSession( 18 | data 19 | ); 20 | 21 | if (!response.data?.success) { 22 | throw new Error(response.data.message || globalMessages.smthWentWrong); 23 | } 24 | 25 | const stripe = await getStripe(); 26 | await stripe.redirectToCheckout({ 27 | sessionId: response.data.data.id, 28 | }); 29 | 30 | return response.data.data; 31 | } catch (error: any) { 32 | console.log(error); 33 | throw error; 34 | } 35 | } 36 | ); 37 | 38 | export const createActiveSubscription = createAsyncThunk( 39 | CREATE_ACTIVE_SUBSCRIPTION, 40 | async (data) => { 41 | try { 42 | const response = await ActiveSubscriptionService.createActiveSubscription< 43 | any, 44 | ICreateActiveSubscriptionPayloadData 45 | >(data); 46 | 47 | if (!response.data?.success) { 48 | throw new Error(response.data.message || globalMessages.smthWentWrong); 49 | } 50 | 51 | return response.data.data; 52 | } catch (error: any) { 53 | console.log(error); 54 | throw error; 55 | } 56 | } 57 | ); 58 | -------------------------------------------------------------------------------- /src/tests/constants/global.ts: -------------------------------------------------------------------------------- 1 | import { IPlan } from 'types'; 2 | 3 | export const mockAuthData = { 4 | firstName: 'John', 5 | lastName: 'Doe', 6 | email: 'john@example.com', 7 | password: 'test1234', 8 | confirmPassword: 'test1234', 9 | }; 10 | 11 | export const mockPlansData: IPlan[] = [ 12 | { 13 | _id: '1', 14 | name: 'Free', 15 | price: 0, 16 | color: 'primary', 17 | planItems: [ 18 | { 19 | _id: '1', 20 | name: 'Happiness', 21 | created_at: new Date().toISOString(), 22 | updated_at: new Date().toISOString(), 23 | }, 24 | ], 25 | created_at: new Date().toISOString(), 26 | updated_at: new Date().toISOString(), 27 | }, 28 | { 29 | _id: '2', 30 | name: 'Premium', 31 | price: 9.99, 32 | color: 'secondary', 33 | planItems: [ 34 | { 35 | _id: '1', 36 | name: 'Happiness', 37 | created_at: new Date().toISOString(), 38 | updated_at: new Date().toISOString(), 39 | }, 40 | ], 41 | created_at: new Date().toISOString(), 42 | updated_at: new Date().toISOString(), 43 | }, 44 | { 45 | _id: '3', 46 | name: 'Business', 47 | price: 19.99, 48 | color: 'subscription-purple', 49 | planItems: [ 50 | { 51 | _id: '1', 52 | name: 'Happiness', 53 | created_at: new Date().toISOString(), 54 | updated_at: new Date().toISOString(), 55 | }, 56 | ], 57 | created_at: new Date().toISOString(), 58 | updated_at: new Date().toISOString(), 59 | }, 60 | { 61 | _id: '4', 62 | name: 'Enterprise', 63 | price: 29.99, 64 | color: 'subscription-blue', 65 | planItems: [ 66 | { 67 | _id: '1', 68 | name: 'Happiness', 69 | created_at: new Date().toISOString(), 70 | updated_at: new Date().toISOString(), 71 | }, 72 | ], 73 | created_at: new Date().toISOString(), 74 | updated_at: new Date().toISOString(), 75 | }, 76 | ]; 77 | -------------------------------------------------------------------------------- /src/pages/user/Home/components/SubscriptionCard/SubscriptionCard.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../assets/styles/abstracts/mixins"; 2 | 3 | 4 | .subscription-card { 5 | color: var(--black); 6 | @include border(all); 7 | @include flex(column, flex-start, flex-start); 8 | border-radius: 1rem; 9 | 10 | &--primary { 11 | border-color: var(--primary); 12 | } 13 | 14 | &--secondary { 15 | border-color: var(--secondary); 16 | } 17 | 18 | &--subscription-purple { 19 | border-color: var(--subscription-purple); 20 | } 21 | 22 | &--subscription-blue { 23 | border-color: var(--subscription-blue); 24 | } 25 | } 26 | .subscription-card__info { 27 | padding: 2rem 1.5rem; 28 | @include flex(column, flex-start, flex-start); 29 | width: 100%; 30 | } 31 | .subscription-card__title { 32 | font-size: 1.5rem; 33 | margin-bottom: 1rem; 34 | 35 | &--primary { 36 | color: var(--primary); 37 | } 38 | 39 | &--secondary { 40 | color: var(--secondary); 41 | } 42 | } 43 | .subscription-card__details { 44 | @include flex(column, flex-start, flex-start); 45 | width: 100%; 46 | } 47 | .subscription-card__amount { 48 | @include flex(row, flex-end, flex-start); 49 | margin-bottom: 1rem; 50 | } 51 | .subscription-card__value { 52 | margin-right: 0.5rem; 53 | font-size: 3rem; 54 | font-weight: 500; 55 | } 56 | .subscription-card__currency { 57 | font-size: 1.25rem; 58 | } 59 | .subscription-card__frequency { 60 | margin-bottom: 1.5rem; 61 | } 62 | .subscription-card__button { 63 | width: 100%; 64 | } 65 | .subscription-card__features { 66 | padding: 2rem 1.5rem; 67 | @include flex(column, flex-start, flex-start); 68 | width: 100%; 69 | } 70 | .subscription-card__feature { 71 | @include border(top, var(--feature-item-border-color)); 72 | @include flex(row, center, flex-start); 73 | width: 100%; 74 | padding: 1rem 0; 75 | 76 | &:first-child { 77 | border-color: var(--black); 78 | } 79 | } 80 | .subscription-card__feature-value { 81 | margin-left: 0.5rem; 82 | } -------------------------------------------------------------------------------- /src/pages/guest/Auth/useAuthContainer.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { yupResolver } from '@hookform/resolvers/yup'; 4 | import { useAppDispatch, useAppSelector } from '../../../store'; 5 | 6 | import { signInSchema, signUpSchema } from 'utils'; 7 | 8 | import { signUp, signIn } from 'store/user/user.action'; 9 | import { selectVerificationData, selectLoading } from 'store/user/user.selectors'; 10 | 11 | import { IUserSignInData, IUserSignUpData } from 'types'; 12 | 13 | const useAuthContainer = () => { 14 | const dispatch = useAppDispatch(); 15 | 16 | const verificationData = useAppSelector(selectVerificationData); 17 | const loading = useAppSelector(selectLoading); 18 | 19 | const [isSignUp, setIsSignUp] = useState(true); 20 | 21 | const { 22 | register, 23 | unregister, 24 | handleSubmit, 25 | formState: { errors }, 26 | } = useForm({ 27 | resolver: yupResolver(isSignUp ? signUpSchema : signInSchema), 28 | }); 29 | 30 | const handleToggleIsSignUp = () => { 31 | setIsSignUp((prevIsSignUp) => !prevIsSignUp); 32 | }; 33 | 34 | const handleFormSubmit = (data: IUserSignInData | IUserSignUpData) => { 35 | if (isSignUp) { 36 | const sendData = data as IUserSignUpData; 37 | dispatch(signUp(sendData)); 38 | } else { 39 | const sendData = data as IUserSignInData; 40 | dispatch(signIn(sendData)); 41 | } 42 | }; 43 | 44 | useEffect(() => { 45 | if (!isSignUp) { 46 | unregister('confirmPassword'); 47 | unregister('firstName'); 48 | unregister('lastName'); 49 | } 50 | }, [isSignUp]); 51 | 52 | return { 53 | isSignUp, 54 | register, 55 | unregister, 56 | handleSubmit, 57 | errors, 58 | handleToggleIsSignUp, 59 | handleFormSubmit, 60 | verificationData, 61 | loading, 62 | }; 63 | }; 64 | 65 | export type UseAuthContainerType = Omit, 'unregister'>; 66 | 67 | export default useAuthContainer; 68 | -------------------------------------------------------------------------------- /src/store/user/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import store from 'store'; 3 | 4 | import { signUp, verifyEmail, signIn } from './user.action'; 5 | 6 | import { IUserState } from './types'; 7 | 8 | const initialState: IUserState = { 9 | userData: null, 10 | accessToken: store.get('accessToken'), 11 | verificationData: null, 12 | isVerificationPassed: false, 13 | error: null, 14 | loading: false, 15 | }; 16 | 17 | const userReducer = createReducer(initialState, (builder) => { 18 | builder 19 | .addCase(signUp.fulfilled, (state, action) => { 20 | state.verificationData = action.payload; 21 | state.error = null; 22 | state.loading = false; 23 | }) 24 | .addCase(signUp.rejected, (state, action) => { 25 | state.loading = false; 26 | state.error = action.error?.message as string; 27 | }) 28 | .addCase(signUp.pending, (state) => { 29 | state.loading = true; 30 | state.error = null; 31 | }) 32 | 33 | .addCase(verifyEmail.fulfilled, (state, action) => { 34 | state.isVerificationPassed = action.payload.isEmailVerified; 35 | state.error = null; 36 | state.loading = false; 37 | }) 38 | .addCase(verifyEmail.pending, (state) => { 39 | state.error = null; 40 | state.loading = true; 41 | }) 42 | .addCase(verifyEmail.rejected, (state, action) => { 43 | state.error = action.error?.message as string; 44 | state.loading = false; 45 | }) 46 | 47 | .addCase(signIn.fulfilled, (state, action) => { 48 | state.accessToken = action.payload.accessToken; 49 | state.userData = action.payload.userData; 50 | state.error = null; 51 | state.loading = false; 52 | }) 53 | .addCase(signIn.pending, (state) => { 54 | state.error = null; 55 | state.loading = true; 56 | }) 57 | .addCase(signIn.rejected, (state, action) => { 58 | state.error = action.error?.message as string; 59 | state.loading = false; 60 | }) 61 | .addDefaultCase((state) => state); 62 | }); 63 | 64 | export default userReducer; 65 | -------------------------------------------------------------------------------- /src/store/user/user.action.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import store from 'store'; 3 | 4 | import UserService from 'services/UserService'; 5 | 6 | import { SIGN_UP, VERIFY_EMAIL, SIGN_IN } from './user.actionTypes'; 7 | 8 | import { globalMessages } from '../../constants'; 9 | 10 | import { 11 | ISignUpPayloadData, 12 | ISignUpReturnData, 13 | IVerifyEmailPayloadData, 14 | IVerifyEmailReturnData, 15 | ISignInPayloadData, 16 | ISignInReturnData, 17 | } from './types'; 18 | 19 | export const signUp = createAsyncThunk(SIGN_UP, async (data) => { 20 | try { 21 | const response = await UserService.signUp(data); 22 | 23 | if (!response.data?.success) { 24 | throw new Error(response.data.message || globalMessages.smthWentWrong); 25 | } 26 | 27 | return response.data.data; 28 | } catch (error: any) { 29 | console.log(error); 30 | throw error; 31 | } 32 | }); 33 | 34 | export const verifyEmail = createAsyncThunk( 35 | VERIFY_EMAIL, 36 | async (data) => { 37 | try { 38 | const response = await UserService.verifyEmail(data.token); 39 | 40 | if (!response.data?.success) { 41 | throw new Error(response.data.message || globalMessages.smthWentWrong); 42 | } 43 | 44 | return response.data.data; 45 | } catch (error: any) { 46 | console.log(error); 47 | throw error; 48 | } 49 | } 50 | ); 51 | 52 | export const signIn = createAsyncThunk(SIGN_IN, async (data) => { 53 | try { 54 | const response = await UserService.signIn(data); 55 | 56 | if (!response.data?.success) { 57 | throw new Error(response.data.message || globalMessages.smthWentWrong); 58 | } 59 | 60 | store.set('accessToken', response.data.data.accessToken); 61 | 62 | return response.data.data; 63 | } catch (error: any) { 64 | console.log(error); 65 | throw error; 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /src/assets/styles/components/_button.scss: -------------------------------------------------------------------------------- 1 | .base-button { 2 | background-color: transparent; 3 | border: none; 4 | padding: 0.5rem 1rem; 5 | border-radius: 0.375rem; 6 | font-size: inherit; 7 | transition: background-color .3s ease-in-out, box-shadow .3s ease-in-out; 8 | 9 | &:disabled { 10 | opacity: .7; 11 | 12 | &:hover { 13 | cursor: not-allowed; 14 | } 15 | } 16 | 17 | &:hover { 18 | cursor: pointer; 19 | } 20 | 21 | &--primary { 22 | background-color: var(--primary); 23 | color: var(--button-white); 24 | 25 | &:hover { 26 | &:not(:disabled) { 27 | background-color: var(--primary-light); 28 | } 29 | } 30 | 31 | &:focus { 32 | &:not(:disabled) { 33 | background-color: var(--primary-dark); 34 | box-shadow: var(--box-shadow-primary); 35 | } 36 | 37 | } 38 | } 39 | 40 | &--secondary { 41 | background-color: var(--secondary); 42 | color: var(--button-white); 43 | 44 | &:hover { 45 | &:not(:disabled) { 46 | background-color: var(--secondary-light); 47 | } 48 | } 49 | 50 | &:focus { 51 | &:not(:disabled) { 52 | background-color: var(--secondary-dark); 53 | box-shadow: var(--box-shadow-secondary); 54 | } 55 | 56 | } 57 | } 58 | 59 | &--subscription-purple { 60 | background-color: var(--subscription-purple); 61 | color: var(--button-white); 62 | 63 | &:hover { 64 | &:not(:disabled) { 65 | background-color: var(--subscription-purple-light); 66 | } 67 | } 68 | 69 | &:focus { 70 | &:not(:disabled) { 71 | background-color: var(--subscription-purple-dark); 72 | box-shadow: var(--box-shadow-subscription-purple); 73 | } 74 | } 75 | } 76 | 77 | &--subscription-blue { 78 | background-color: var(--subscription-blue); 79 | color: var(--button-white); 80 | 81 | &:hover { 82 | &:not(:disabled) { 83 | background-color: var(--subscription-blue-light); 84 | } 85 | } 86 | 87 | &:focus { 88 | &:not(:disabled) { 89 | background-color: var(--subscription-blue-dark); 90 | box-shadow: var(--box-shadow-subscription-blue); 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkinator-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@hookform/resolvers": "^3.1.1", 7 | "@reduxjs/toolkit": "^1.9.5", 8 | "@stripe/stripe-js": "^2.1.5", 9 | "@testing-library/jest-dom": "^5.16.1", 10 | "@testing-library/react": "^13.0.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "@types/jest": "^27.4.0", 13 | "@types/node": "^17.0.10", 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-hook-form": "^7.45.2", 19 | "react-redux": "^8.1.1", 20 | "react-router-dom": "^6.14.2", 21 | "react-scripts": "5.0.1", 22 | "store": "^2.0.12", 23 | "typescript": "^4.4.2", 24 | "web-vitals": "^2.1.4", 25 | "workbox-background-sync": "^6.4.2", 26 | "workbox-broadcast-update": "^6.4.2", 27 | "workbox-cacheable-response": "^6.4.2", 28 | "workbox-core": "^6.4.2", 29 | "workbox-expiration": "^6.4.2", 30 | "workbox-google-analytics": "^6.4.2", 31 | "workbox-navigation-preload": "^6.4.2", 32 | "workbox-precaching": "^6.4.2", 33 | "workbox-range-requests": "^6.4.2", 34 | "workbox-routing": "^6.4.2", 35 | "workbox-strategies": "^6.4.2", 36 | "workbox-streams": "^6.4.2", 37 | "yup": "^1.2.0" 38 | }, 39 | "scripts": { 40 | "start": "react-scripts start", 41 | "build": "react-scripts build", 42 | "test": "react-scripts test", 43 | "eject": "react-scripts eject", 44 | "lint": "eslint . --ext .ts --ext .tsx", 45 | "lint:fix": "eslint --fix . --ext .ts --ext .tsx" 46 | }, 47 | "eslintConfig": { 48 | "extends": [ 49 | "react-app", 50 | "react-app/jest" 51 | ] 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all" 58 | ], 59 | "development": [ 60 | "last 1 chrome version", 61 | "last 1 firefox version", 62 | "last 1 safari version" 63 | ] 64 | }, 65 | "devDependencies": { 66 | "@types/store": "^2.0.2", 67 | "@typescript-eslint/eslint-plugin": "^5.50.0", 68 | "@typescript-eslint/parser": "^6.1.0", 69 | "axios": "^1.4.0", 70 | "eslint": "^8.0.1", 71 | "eslint-config-prettier": "^8.6.0", 72 | "eslint-config-react": "^1.1.7", 73 | "eslint-config-react-app": "^7.0.1", 74 | "eslint-config-standard-with-typescript": "^36.1.0", 75 | "eslint-plugin-import": "^2.25.2", 76 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", 77 | "eslint-plugin-prettier": "^4.2.1", 78 | "eslint-plugin-promise": "^6.0.0", 79 | "eslint-plugin-react": "^7.32.2", 80 | "prettier": "^2.8.3", 81 | "sass": "^1.64.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | // This service worker can be customized! 5 | // See https://developers.google.com/web/tools/workbox/modules 6 | // for the list of available Workbox modules, or add any other 7 | // code you'd like. 8 | // You can also remove this file if you'd prefer not to use a 9 | // service worker, and the Workbox build step will be skipped. 10 | 11 | import { clientsClaim } from 'workbox-core'; 12 | import { ExpirationPlugin } from 'workbox-expiration'; 13 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 14 | import { registerRoute } from 'workbox-routing'; 15 | import { StaleWhileRevalidate } from 'workbox-strategies'; 16 | 17 | declare const self: ServiceWorkerGlobalScope; 18 | 19 | clientsClaim(); 20 | 21 | // Precache all of the assets generated by your build process. 22 | // Their URLs are injected into the manifest variable below. 23 | // This variable must be present somewhere in your service worker file, 24 | // even if you decide not to use precaching. See https://cra.link/PWA 25 | precacheAndRoute(self.__WB_MANIFEST); 26 | 27 | // Set up App Shell-style routing, so that all navigation requests 28 | // are fulfilled with your index.html shell. Learn more at 29 | // https://developers.google.com/web/fundamentals/architecture/app-shell 30 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 31 | registerRoute( 32 | // Return false to exempt requests from being fulfilled by index.html. 33 | ({ request, url }: { request: Request; url: URL }) => { 34 | // If this isn't a navigation, skip. 35 | if (request.mode !== 'navigate') { 36 | return false; 37 | } 38 | 39 | // If this is a URL that starts with /_, skip. 40 | if (url.pathname.startsWith('/_')) { 41 | return false; 42 | } 43 | 44 | // If this looks like a URL for a resource, because it contains 45 | // a file extension, skip. 46 | if (url.pathname.match(fileExtensionRegexp)) { 47 | return false; 48 | } 49 | 50 | // Return true to signal that we want to use the handler. 51 | return true; 52 | }, 53 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 54 | ); 55 | 56 | // An example runtime caching route for requests that aren't handled by the 57 | // precache, in this case same-origin .png requests like those from in public/ 58 | registerRoute( 59 | // Add in any other file extensions or routing criteria as needed. 60 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), 61 | // Customize this strategy as needed, e.g., by changing to CacheFirst. 62 | new StaleWhileRevalidate({ 63 | cacheName: 'images', 64 | plugins: [ 65 | // Ensure that once this runtime cache reaches a maximum size the 66 | // least-recently used images are removed. 67 | new ExpirationPlugin({ maxEntries: 50 }), 68 | ], 69 | }) 70 | ); 71 | 72 | // This allows the web app to trigger skipWaiting via 73 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 74 | self.addEventListener('message', (event) => { 75 | if (event.data && event.data.type === 'SKIP_WAITING') { 76 | self.skipWaiting(); 77 | } 78 | }); 79 | 80 | // Any other custom service worker logic can go here. 81 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/__tests__/Auth.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent, waitFor } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { act } from 'react-dom/test-utils'; 4 | 5 | import Auth from '../Auth'; 6 | 7 | import { mockAuthData } from 'tests/constants'; 8 | 9 | describe('Auth', () => { 10 | it('should work correctly for sign up', async () => { 11 | const mockHandleFormSubmit = jest.fn(); 12 | 13 | const { getByLabelText, getByText } = render( 14 | 15 | 25 | 26 | ); 27 | 28 | // Interact with input fields 29 | fireEvent.change(getByLabelText('First Name'), { target: { value: mockAuthData.firstName } }); 30 | fireEvent.change(getByLabelText('Last Name'), { target: { value: mockAuthData.lastName } }); 31 | fireEvent.change(getByLabelText('Email'), { target: { value: mockAuthData.email } }); 32 | fireEvent.change(getByLabelText('Password'), { target: { value: mockAuthData.password } }); 33 | fireEvent.change(getByLabelText('Confirm Password'), { target: { value: mockAuthData.confirmPassword } }); 34 | 35 | // Submit the form 36 | await act(async () => { 37 | fireEvent.click(getByText('Sign Up')); 38 | }); 39 | 40 | // Check if the form submission function was called 41 | await waitFor(() => { 42 | expect(mockHandleFormSubmit).toHaveBeenCalled(); 43 | }); 44 | }); 45 | 46 | it('should work correctly for sign in', async () => { 47 | const mockHandleFormSubmit = jest.fn(); 48 | 49 | const { getByLabelText, getByText } = render( 50 | 51 | 61 | 62 | ); 63 | 64 | // Interact with input fields 65 | fireEvent.change(getByLabelText('Email'), { target: { value: mockAuthData.email } }); 66 | fireEvent.change(getByLabelText('Password'), { target: { value: mockAuthData.password } }); 67 | 68 | // Submit the form 69 | await act(async () => { 70 | fireEvent.click(getByText('Sign In')); 71 | }); 72 | 73 | // Check if the form submission function was called 74 | await waitFor(() => { 75 | expect(mockHandleFormSubmit).toHaveBeenCalled(); 76 | }); 77 | }); 78 | 79 | it('should contain link to forgot password', () => { 80 | const { getByText } = render( 81 | 82 | 92 | 93 | ); 94 | 95 | const link = getByText(/Forgot password\?/i); 96 | expect(link).toBeTruthy(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/pages/guest/Auth/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FieldErrors } from 'react-hook-form'; 3 | 4 | import { Button, Input, Link } from 'components'; 5 | 6 | import { IAuthProps } from './types'; 7 | import { IUserSignUpData } from 'types'; 8 | 9 | import styles from './Auth.module.scss'; 10 | 11 | const Auth: FC = ({ 12 | isSignUp, 13 | handleToggleIsSignUp, 14 | handleFormSubmit, 15 | errors, 16 | register, 17 | handleSubmit, 18 | verificationData, 19 | loading, 20 | }) => { 21 | return ( 22 | 23 | {isSignUp ? 'Create account' : 'Log In'} 24 | 25 | {isSignUp && ( 26 | 27 | 28 | First Name 29 | 30 | ).firstName?.message} 33 | id="firstName" 34 | /> 35 | 36 | )} 37 | {isSignUp && ( 38 | 39 | 40 | Last Name 41 | 42 | ).lastName?.message} 45 | id="lastName" 46 | /> 47 | 48 | )} 49 | 50 | 51 | Email 52 | 53 | 54 | 55 | 56 | 57 | Password 58 | 59 | 60 | 61 | {isSignUp && ( 62 | 63 | 64 | Confirm Password 65 | 66 | ).confirmPassword?.message} 70 | id="confirmPassword" 71 | /> 72 | 73 | )} 74 | 75 | {verificationData && ( 76 | 77 | Verification email is sent to your email {verificationData.email} 78 | 79 | )} 80 | 81 | 82 | 83 | Sign {isSignUp ? 'Up' : 'In'} 84 | 85 | 86 | {isSignUp ? 'Log in' : 'Create account'} 87 | 88 | 89 | 90 | Forgot password? 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default Auth; 98 | -------------------------------------------------------------------------------- /src/serviceWorkerRegistration.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | type Config = { 22 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 23 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 24 | }; 25 | 26 | export function register(config?: Config) { 27 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 28 | // The URL constructor is available in all browsers that support SW. 29 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 30 | if (publicUrl.origin !== window.location.origin) { 31 | // Our service worker won't work if PUBLIC_URL is on a different origin 32 | // from what our page is served on. This might happen if a CDN is used to 33 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 34 | return; 35 | } 36 | 37 | window.addEventListener('load', () => { 38 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 39 | 40 | if (isLocalhost) { 41 | // This is running on localhost. Let's check if a service worker still exists or not. 42 | checkValidServiceWorker(swUrl, config); 43 | 44 | // Add some additional logging to localhost, pointing developers to the 45 | // service worker/PWA documentation. 46 | navigator.serviceWorker.ready.then(() => { 47 | console.log( 48 | 'This web app is being served cache-first by a service ' + 49 | 'worker. To learn more, visit https://cra.link/PWA' 50 | ); 51 | }); 52 | } else { 53 | // Is not localhost. Just register service worker 54 | registerValidSW(swUrl, config); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | function registerValidSW(swUrl: string, config?: Config) { 61 | navigator.serviceWorker 62 | .register(swUrl) 63 | .then((registration) => { 64 | registration.onupdatefound = () => { 65 | const installingWorker = registration.installing; 66 | if (installingWorker == null) { 67 | return; 68 | } 69 | installingWorker.onstatechange = () => { 70 | if (installingWorker.state === 'installed') { 71 | if (navigator.serviceWorker.controller) { 72 | // At this point, the updated precached content has been fetched, 73 | // but the previous service worker will still serve the older 74 | // content until all client tabs are closed. 75 | console.log( 76 | 'New content is available and will be used when all ' + 77 | 'tabs for this page are closed. See https://cra.link/PWA.' 78 | ); 79 | 80 | // Execute callback 81 | if (config && config.onUpdate) { 82 | config.onUpdate(registration); 83 | } 84 | } else { 85 | // At this point, everything has been precached. 86 | // It's the perfect time to display a 87 | // "Content is cached for offline use." message. 88 | console.log('Content is cached for offline use.'); 89 | 90 | // Execute callback 91 | if (config && config.onSuccess) { 92 | config.onSuccess(registration); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | }) 99 | .catch((error) => { 100 | console.error('Error during service worker registration:', error); 101 | }); 102 | } 103 | 104 | function checkValidServiceWorker(swUrl: string, config?: Config) { 105 | // Check if the service worker can be found. If it can't reload the page. 106 | fetch(swUrl, { 107 | headers: { 'Service-Worker': 'script' }, 108 | }) 109 | .then((response) => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get('content-type'); 112 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log('No internet connection found. App is running in offline mode.'); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready 132 | .then((registration) => { 133 | registration.unregister(); 134 | }) 135 | .catch((error) => { 136 | console.error(error.message); 137 | }); 138 | } 139 | } 140 | --------------------------------------------------------------------------------
Email verified successfully.
77 | Verification email is sent to your email {verificationData.email} 78 |