├── src
├── widgets
│ ├── PageError
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── PageError.module.scss
│ │ │ ├── PageError.tsx
│ │ │ └── PageError.stories.tsx
│ ├── PageLoader
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── PageLoader.module.scss
│ │ │ └── PageLoader.tsx
│ ├── Sidebar
│ │ ├── index.ts
│ │ ├── ui
│ │ │ ├── SidebarItem
│ │ │ │ ├── SidebarItem.module.scss
│ │ │ │ └── SidebarItem.tsx
│ │ │ └── Sidebar
│ │ │ │ ├── Sidebar.test.tsx
│ │ │ │ ├── Sidebar.stories.tsx
│ │ │ │ ├── Sidebar.module.scss
│ │ │ │ └── Sidebar.tsx
│ │ └── model
│ │ │ └── items.ts
│ ├── ThemeSwitcher
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── ThemeSwitcher.stories.tsx
│ ├── Navbar
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── Navbar.module.scss
│ │ │ ├── Navbar.stories.tsx
│ │ │ └── Navbar.tsx
│ └── LangSwitcher
│ │ └── LangSwitcher.tsx
├── shared
│ ├── const
│ │ ├── localstorage.ts
│ │ └── common.ts
│ ├── config
│ │ ├── storybook
│ │ │ ├── StyleDecorator
│ │ │ │ └── StyleDecorator.ts
│ │ │ ├── RouterDecorator
│ │ │ │ └── RouterDecorator.tsx
│ │ │ ├── ThemeDecorator
│ │ │ │ └── ThemeDecorator.tsx
│ │ │ └── StoreDecorator
│ │ │ │ └── StoreDecorator.tsx
│ │ ├── i18n
│ │ │ ├── i18nForTests.ts
│ │ │ └── i18n.ts
│ │ └── routeConfig
│ │ │ └── routeConfig.tsx
│ ├── lib
│ │ ├── hooks
│ │ │ └── useAppDispatch
│ │ │ │ └── useAppDispatch.ts
│ │ ├── classNames
│ │ │ ├── classNames.ts
│ │ │ └── classNames.test.ts
│ │ ├── tests
│ │ │ ├── renderWithTranslation
│ │ │ │ └── renderWithTranslation.tsx
│ │ │ ├── TestAsyncThunk
│ │ │ │ └── TestAsyncThunk.ts
│ │ │ └── componentRender
│ │ │ │ └── componentRender.tsx
│ │ └── components
│ │ │ └── DynamicModuleLoader
│ │ │ └── DynamicModuleLoader.tsx
│ ├── ui
│ │ ├── AppLink
│ │ │ ├── AppLink.module.scss
│ │ │ ├── AppLink.tsx
│ │ │ └── AppLink.stories.tsx
│ │ ├── Text
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.tsx
│ │ │ └── Text.stories.tsx
│ │ ├── Portal
│ │ │ └── Portal.tsx
│ │ ├── Loader
│ │ │ ├── Loader.tsx
│ │ │ ├── Loader.stories.tsx
│ │ │ └── Loader.scss
│ │ ├── Input
│ │ │ ├── Input.stories.tsx
│ │ │ ├── Input.module.scss
│ │ │ └── Input.tsx
│ │ ├── Button
│ │ │ ├── Button.test.tsx
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.tsx
│ │ │ └── Button.stories.tsx
│ │ └── Modal
│ │ │ ├── Modal.stories.tsx
│ │ │ ├── Modal.module.scss
│ │ │ └── Modal.tsx
│ └── assets
│ │ └── icons
│ │ ├── main-20-20.svg
│ │ ├── about-20-20.svg
│ │ ├── theme-dark.svg
│ │ ├── theme-light.svg
│ │ └── profile-20-20.svg
├── pages
│ ├── NotFoundPage
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── NotFoundPage.module.scss
│ │ │ ├── NotFoundPage.tsx
│ │ │ └── NotFoundPage.stories.tsx
│ ├── MainPage
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── MainPage.async.tsx
│ │ │ ├── MainPage.tsx
│ │ │ └── MainPage.stories.tsx
│ ├── AboutPage
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── AboutPage.async.tsx
│ │ │ ├── AboutPage.tsx
│ │ │ └── AboutPage.stories.tsx
│ └── ProfilePage
│ │ ├── index.ts
│ │ └── ui
│ │ ├── ProfilePage.async.tsx
│ │ ├── ProfilePage.tsx
│ │ └── ProfilePage.stories.tsx
├── app
│ ├── providers
│ │ ├── router
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ └── AppRouter.tsx
│ │ ├── ErrorBoundary
│ │ │ ├── index.ts
│ │ │ └── ui
│ │ │ │ ├── BugButton.tsx
│ │ │ │ └── ErrorBoundary.tsx
│ │ ├── ThemeProvider
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── ThemeContext.ts
│ │ │ │ └── useTheme.ts
│ │ │ └── ui
│ │ │ │ └── ThemeProvider.tsx
│ │ └── StoreProvider
│ │ │ ├── index.ts
│ │ │ ├── ui
│ │ │ └── StoreProvider.tsx
│ │ │ └── config
│ │ │ ├── store.ts
│ │ │ ├── StateSchema.ts
│ │ │ └── reduceManager.ts
│ ├── styles
│ │ ├── reset.scss
│ │ ├── themes
│ │ │ ├── light.scss
│ │ │ └── dark.scss
│ │ ├── index.scss
│ │ └── variables
│ │ │ └── global.scss
│ ├── types
│ │ └── global.d.ts
│ └── App.tsx
├── entities
│ ├── Counter
│ │ ├── model
│ │ │ ├── types
│ │ │ │ └── counterSchema.ts
│ │ │ ├── selectors
│ │ │ │ ├── getCounter
│ │ │ │ │ ├── getCounter.ts
│ │ │ │ │ └── getCounter.test.ts
│ │ │ │ └── getCounterValue
│ │ │ │ │ ├── getCounterValue.ts
│ │ │ │ │ └── getCounterValue.test.ts
│ │ │ └── slice
│ │ │ │ ├── counterSlice.ts
│ │ │ │ └── counterSlice.test.ts
│ │ ├── index.ts
│ │ └── ui
│ │ │ ├── Counter.tsx
│ │ │ └── Counter.test.tsx
│ ├── Profile
│ │ ├── index.ts
│ │ └── model
│ │ │ ├── types
│ │ │ └── profile.ts
│ │ │ └── slice
│ │ │ └── profileSlice.ts
│ └── User
│ │ ├── model
│ │ ├── types
│ │ │ └── user.ts
│ │ ├── selectors
│ │ │ └── getUserAuthData
│ │ │ │ └── getUserAuthData.ts
│ │ └── slice
│ │ │ └── userSlice.ts
│ │ └── index.ts
├── features
│ └── AuthByUsername
│ │ ├── index.ts
│ │ ├── model
│ │ ├── types
│ │ │ └── loginSchema.ts
│ │ ├── selectors
│ │ │ ├── getLoginError
│ │ │ │ ├── getLoginError.ts
│ │ │ │ └── getLoginError.test.ts
│ │ │ ├── getLoginPassword
│ │ │ │ ├── getLoginPassword.ts
│ │ │ │ └── getLoginPassword.test.ts
│ │ │ ├── getLoginUsername
│ │ │ │ ├── getLoginUsername.ts
│ │ │ │ └── getLoginUsername.test.ts
│ │ │ └── getLoginIsLoading
│ │ │ │ ├── getLoginIsLoading.ts
│ │ │ │ └── getLoginIsLoading.test.ts
│ │ ├── slice
│ │ │ ├── loginSlice.test.ts
│ │ │ └── loginSlice.ts
│ │ └── services
│ │ │ └── loginByUsername
│ │ │ ├── loginByUsername.ts
│ │ │ └── loginByUsername.test.ts
│ │ └── ui
│ │ ├── LoginForm
│ │ ├── LoginForm.async.ts
│ │ ├── LoginForm.module.scss
│ │ ├── LoginForm.stories.tsx
│ │ └── LoginForm.tsx
│ │ └── LoginModal
│ │ └── LoginModal.tsx
└── index.tsx
├── .loki
├── .gitignore
└── reference
│ ├── chrome_laptop_shared_Modal_Dark.png
│ ├── chrome_laptop_shared_Text_Error.png
│ ├── chrome_iphone7_pages_MainPage_Dark.png
│ ├── chrome_iphone7_shared_AppLink_Red.png
│ ├── chrome_iphone7_shared_Button_Clear.png
│ ├── chrome_iphone7_shared_Loader_Dark.png
│ ├── chrome_iphone7_shared_Modal_Dark.png
│ ├── chrome_iphone7_shared_Text_Error.png
│ ├── chrome_iphone7_shared_Text_Primary.png
│ ├── chrome_iphone7_widget_Navbar_Dark.png
│ ├── chrome_iphone7_widget_Navbar_Light.png
│ ├── chrome_iphone7_widget_Sidebar_Dark.png
│ ├── chrome_laptop_pages_AboutPage_Dark.png
│ ├── chrome_laptop_pages_MainPage_Dark.png
│ ├── chrome_laptop_shared_AppLink_Red.png
│ ├── chrome_laptop_shared_Button_Clear.png
│ ├── chrome_laptop_shared_Button_Square.png
│ ├── chrome_laptop_shared_Input_Primary.png
│ ├── chrome_laptop_shared_Loader_Dark.png
│ ├── chrome_laptop_shared_Loader_Normal.png
│ ├── chrome_laptop_shared_Modal_Primary.png
│ ├── chrome_laptop_shared_Text_Primary.png
│ ├── chrome_laptop_widget_Navbar_Dark.png
│ ├── chrome_laptop_widget_Navbar_Light.png
│ ├── chrome_laptop_widget_Sidebar_Dark.png
│ ├── chrome_laptop_widget_Sidebar_Light.png
│ ├── chrome_iphone7_pages_AboutPage_Dark.png
│ ├── chrome_iphone7_pages_MainPage_Normal.png
│ ├── chrome_iphone7_shared_Button_Primary.png
│ ├── chrome_iphone7_shared_Button_Square.png
│ ├── chrome_iphone7_shared_Input_Primary.png
│ ├── chrome_iphone7_shared_Loader_Normal.png
│ ├── chrome_iphone7_shared_Modal_Primary.png
│ ├── chrome_iphone7_shared_Text_Only_Text.png
│ ├── chrome_iphone7_widget_PageError_Dark.png
│ ├── chrome_iphone7_widget_Sidebar_Light.png
│ ├── chrome_laptop_pages_AboutPage_Normal.png
│ ├── chrome_laptop_pages_MainPage_Normal.png
│ ├── chrome_laptop_shared_AppLink_Primary.png
│ ├── chrome_laptop_shared_Button_Disabled.png
│ ├── chrome_laptop_shared_Button_Outlined.png
│ ├── chrome_laptop_shared_Button_Primary.png
│ ├── chrome_laptop_shared_Text_Only_Text.png
│ ├── chrome_laptop_shared_Text_Only_Title.png
│ ├── chrome_laptop_widget_PageError_Dark.png
│ ├── chrome_laptop_widget_PageError_Light.png
│ ├── chrome_iphone7_pages_AboutPage_Normal.png
│ ├── chrome_iphone7_pages_NotFoundPage_Dark.png
│ ├── chrome_iphone7_shared_AppLink_Primary.png
│ ├── chrome_iphone7_shared_AppLink_Red_Dark.png
│ ├── chrome_iphone7_shared_AppLink_Secondary.png
│ ├── chrome_iphone7_shared_Button_Disabled.png
│ ├── chrome_iphone7_shared_Button_Outlined.png
│ ├── chrome_iphone7_shared_Text_Only_Title.png
│ ├── chrome_iphone7_shared_Text_Primary_Dark.png
│ ├── chrome_iphone7_widget_PageError_Light.png
│ ├── chrome_laptop_pages_NotFoundPage_Dark.png
│ ├── chrome_laptop_pages_NotFoundPage_Normal.png
│ ├── chrome_laptop_shared_AppLink_Red_Dark.png
│ ├── chrome_laptop_shared_AppLink_Secondary.png
│ ├── chrome_laptop_shared_Text_Primary_Dark.png
│ ├── chrome_iphone7_features_LoginForm_Loading.png
│ ├── chrome_iphone7_features_LoginForm_Primary.png
│ ├── chrome_iphone7_pages_NotFoundPage_Normal.png
│ ├── chrome_iphone7_shared_Text_Only_Text_Dark.png
│ ├── chrome_iphone7_widgets_ThemeSwitcher_Dark.png
│ ├── chrome_laptop_features_LoginForm_Loading.png
│ ├── chrome_laptop_features_LoginForm_Primary.png
│ ├── chrome_laptop_shared_AppLink_Primary_Dark.png
│ ├── chrome_laptop_shared_Button_Outlined_Dark.png
│ ├── chrome_laptop_shared_Button_Square_Size_L.png
│ ├── chrome_laptop_shared_Button_Square_Size_M.png
│ ├── chrome_laptop_shared_Text_Only_Text_Dark.png
│ ├── chrome_laptop_shared_Text_Only_Title_Dark.png
│ ├── chrome_laptop_widgets_ThemeSwitcher_Dark.png
│ ├── chrome_iphone7_features_LoginForm_With_Error.png
│ ├── chrome_iphone7_shared_AppLink_Primary_Dark.png
│ ├── chrome_iphone7_shared_AppLink_Secondary_Dark.png
│ ├── chrome_iphone7_shared_Button_Clear_Inverted.png
│ ├── chrome_iphone7_shared_Button_Outlined_Dark.png
│ ├── chrome_iphone7_shared_Button_Outlined_Size_L.png
│ ├── chrome_iphone7_shared_Button_Square_Size_L.png
│ ├── chrome_iphone7_shared_Button_Square_Size_M.png
│ ├── chrome_iphone7_shared_Button_Square_Size_XL.png
│ ├── chrome_iphone7_shared_Text_Only_Title_Dark.png
│ ├── chrome_iphone7_widgets_ThemeSwitcher_Normal.png
│ ├── chrome_laptop_features_LoginForm_With_Error.png
│ ├── chrome_laptop_shared_AppLink_Secondary_Dark.png
│ ├── chrome_laptop_shared_Button_Background_Theme.png
│ ├── chrome_laptop_shared_Button_Clear_Inverted.png
│ ├── chrome_laptop_shared_Button_Outlined_Size_L.png
│ ├── chrome_laptop_shared_Button_Outlined_Size_XL.png
│ ├── chrome_laptop_shared_Button_Square_Size_XL.png
│ ├── chrome_laptop_widgets_ThemeSwitcher_Normal.png
│ ├── chrome_iphone7_shared_Button_Background_Theme.png
│ ├── chrome_iphone7_shared_Button_Outlined_Size_XL.png
│ ├── chrome_laptop_widget_Navbar_Aurhenticated_User.png
│ ├── chrome_iphone7_shared_Button_Background_Inverted.png
│ ├── chrome_iphone7_widget_Navbar_Aurhenticated_User.png
│ └── chrome_laptop_shared_Button_Background_Inverted.png
├── public
├── locales
│ ├── en
│ │ ├── about.json
│ │ ├── main.json
│ │ └── translation.json
│ └── ru
│ │ ├── about.json
│ │ ├── main.json
│ │ └── translation.json
└── index.html
├── extractedTranslations
├── en
│ ├── about.json
│ └── translation.json
└── ru
│ ├── about.json
│ └── translation.json
├── config
├── jest
│ ├── setupTests.ts
│ ├── jestEmptyComponent.tsx
│ └── jest.config.ts
├── storybook
│ ├── main.js
│ ├── preview.js
│ └── webpack.config.ts
└── build
│ ├── buildDevServer.ts
│ ├── buildResolvers.ts
│ ├── types
│ └── config.ts
│ ├── loaders
│ └── buildCssLoader.ts
│ ├── buildWebpackConfig.ts
│ ├── buildPlugins.ts
│ └── buildLoaders.ts
├── .prettierrc.json
├── .gitignore
├── .stylelintrc.json
├── .husky
└── pre-commit
├── babel.config.json
├── json-server
├── db.json
└── index.js
├── webpack.config.ts
├── tsconfig.json
├── scripts
└── generate-visual-json-report.js
├── .github
└── workflows
│ └── main.yaml
├── .eslintrc.js
└── package.json
/src/widgets/PageError/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.loki/.gitignore:
--------------------------------------------------------------------------------
1 | current
2 | difference
3 |
--------------------------------------------------------------------------------
/public/locales/en/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "About page"
3 | }
4 |
--------------------------------------------------------------------------------
/public/locales/en/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "Main page": "Main page"
3 | }
4 |
--------------------------------------------------------------------------------
/public/locales/ru/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "О сайте"
3 | }
4 |
--------------------------------------------------------------------------------
/public/locales/ru/main.json:
--------------------------------------------------------------------------------
1 | {
2 | "Main page": "Главная страница"
3 | }
4 |
--------------------------------------------------------------------------------
/extractedTranslations/en/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "About page"
3 | }
4 |
--------------------------------------------------------------------------------
/extractedTranslations/ru/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "About page"
3 | }
4 |
--------------------------------------------------------------------------------
/src/shared/const/localstorage.ts:
--------------------------------------------------------------------------------
1 | export const USER_LOCALSTORAGE_KEY = 'user';
2 |
--------------------------------------------------------------------------------
/src/widgets/PageLoader/index.ts:
--------------------------------------------------------------------------------
1 | export { PageLoader } from './ui/PageLoader';
2 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/index.ts:
--------------------------------------------------------------------------------
1 | export { Sidebar } from './ui/Sidebar/Sidebar';
2 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage/index.ts:
--------------------------------------------------------------------------------
1 | export { NotFoundPage } from './ui/NotFoundPage';
2 |
--------------------------------------------------------------------------------
/src/widgets/ThemeSwitcher/index.ts:
--------------------------------------------------------------------------------
1 | export { ThemeSwitcher } from './ui/ThemeSwitcher';
2 |
--------------------------------------------------------------------------------
/src/pages/MainPage/index.ts:
--------------------------------------------------------------------------------
1 | export { MainPageAsync as MainPage } from './ui/MainPage.async';
2 |
--------------------------------------------------------------------------------
/src/widgets/Navbar/index.ts:
--------------------------------------------------------------------------------
1 | import { Navbar } from './ui/Navbar';
2 |
3 | export { Navbar };
4 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/index.ts:
--------------------------------------------------------------------------------
1 | export { AboutPageAsync as AboutPage } from './ui/AboutPage.async';
2 |
--------------------------------------------------------------------------------
/config/jest/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import 'regenerator-runtime/runtime';
3 |
--------------------------------------------------------------------------------
/src/app/providers/router/index.ts:
--------------------------------------------------------------------------------
1 | import AppRouter from './ui/AppRouter';
2 |
3 | export { AppRouter };
4 |
--------------------------------------------------------------------------------
/src/pages/ProfilePage/index.ts:
--------------------------------------------------------------------------------
1 | export { ProfilePageAsync as ProfilePage } from './ui/ProfilePage.async';
2 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/types/counterSchema.ts:
--------------------------------------------------------------------------------
1 | export interface CounterSchema {
2 | value: number;
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "printWidth": 90
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/index.ts:
--------------------------------------------------------------------------------
1 | export { LoginModal } from './ui/LoginModal/LoginModal';
2 | export { LoginSchema } from './model/types/loginSchema';
3 |
--------------------------------------------------------------------------------
/src/pages/MainPage/ui/MainPage.async.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 |
3 | export const MainPageAsync = lazy(async () => await import('./MainPage'));
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /build
3 | uploads/
4 | files/
5 | ssl/
6 | logs/
7 | .env
8 | /storybook-static
9 | .loki/report.html
10 | .loki/report.json
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Modal_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Modal_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Error.png
--------------------------------------------------------------------------------
/src/pages/AboutPage/ui/AboutPage.async.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 |
3 | export const AboutPageAsync = lazy(async () => await import('./AboutPage'));
4 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_MainPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_MainPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Red.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Clear.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Loader_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Loader_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Modal_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Modal_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Error.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_Navbar_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_Navbar_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_Navbar_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_Navbar_Light.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_Sidebar_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_Sidebar_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_AboutPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_AboutPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_MainPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_MainPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Red.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Clear.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Square.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Input_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Input_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Loader_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Loader_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Loader_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Loader_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Modal_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Modal_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_Navbar_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_Navbar_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_Navbar_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_Navbar_Light.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_Sidebar_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_Sidebar_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_Sidebar_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_Sidebar_Light.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_AboutPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_AboutPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_MainPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_MainPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Square.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Input_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Input_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Loader_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Loader_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Modal_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Modal_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Only_Text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Only_Text.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_PageError_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_PageError_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_Sidebar_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_Sidebar_Light.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_AboutPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_AboutPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_MainPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_MainPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Disabled.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Outlined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Outlined.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Only_Text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Only_Text.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Only_Title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Only_Title.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_PageError_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_PageError_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_PageError_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_PageError_Light.png
--------------------------------------------------------------------------------
/src/pages/ProfilePage/ui/ProfilePage.async.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 |
3 | export const ProfilePageAsync = lazy(async () => await import('./ProfilePage'));
4 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_AboutPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_AboutPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_NotFoundPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_NotFoundPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Red_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Red_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Secondary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Secondary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Disabled.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Outlined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Outlined.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Only_Title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Only_Title.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Primary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Primary_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_PageError_Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_PageError_Light.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_NotFoundPage_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_NotFoundPage_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_pages_NotFoundPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_pages_NotFoundPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Red_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Red_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Secondary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Secondary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Primary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Primary_Dark.png
--------------------------------------------------------------------------------
/src/entities/Profile/index.ts:
--------------------------------------------------------------------------------
1 | export { Profile, ProfileSchema } from './model/types/profile';
2 | export { profileActions, profileReducer } from './model/slice/profileSlice';
3 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_features_LoginForm_Loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_features_LoginForm_Loading.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_features_LoginForm_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_features_LoginForm_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_pages_NotFoundPage_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_pages_NotFoundPage_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Only_Text_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Only_Text_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widgets_ThemeSwitcher_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widgets_ThemeSwitcher_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_features_LoginForm_Loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_features_LoginForm_Loading.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_features_LoginForm_Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_features_LoginForm_Primary.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Primary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Primary_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Outlined_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Outlined_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Square_Size_L.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Square_Size_L.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Square_Size_M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Square_Size_M.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Only_Text_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Only_Text_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Text_Only_Title_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Text_Only_Title_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widgets_ThemeSwitcher_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widgets_ThemeSwitcher_Dark.png
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "ignoreFiles": ["build/**/*.css"],
4 | "rules": {
5 | "selector-class-pattern": null
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/config/jest/jestEmptyComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const jestEmptyComponent = () => {
4 | return
;
5 | };
6 |
7 | export default jestEmptyComponent;
8 |
--------------------------------------------------------------------------------
/src/entities/User/model/types/user.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: string;
3 | username: string;
4 | }
5 |
6 | export interface UserSchema {
7 | authData?: User;
8 | }
9 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_features_LoginForm_With_Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_features_LoginForm_With_Error.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Primary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Primary_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_AppLink_Secondary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_AppLink_Secondary_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Clear_Inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Clear_Inverted.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Outlined_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Outlined_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Outlined_Size_L.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Outlined_Size_L.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Square_Size_L.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Square_Size_L.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Square_Size_M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Square_Size_M.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Square_Size_XL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Square_Size_XL.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Text_Only_Title_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Text_Only_Title_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widgets_ThemeSwitcher_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widgets_ThemeSwitcher_Normal.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_features_LoginForm_With_Error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_features_LoginForm_With_Error.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_AppLink_Secondary_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_AppLink_Secondary_Dark.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Background_Theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Background_Theme.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Clear_Inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Clear_Inverted.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Outlined_Size_L.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Outlined_Size_L.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Outlined_Size_XL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Outlined_Size_XL.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Square_Size_XL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Square_Size_XL.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widgets_ThemeSwitcher_Normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widgets_ThemeSwitcher_Normal.png
--------------------------------------------------------------------------------
/src/app/providers/ErrorBoundary/index.ts:
--------------------------------------------------------------------------------
1 | import ErrorBoundary from './ui/ErrorBoundary';
2 | import { BugButton } from './ui/BugButton';
3 |
4 | export { ErrorBoundary, BugButton };
5 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Background_Theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Background_Theme.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Outlined_Size_XL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Outlined_Size_XL.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_widget_Navbar_Aurhenticated_User.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_widget_Navbar_Aurhenticated_User.png
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/types/loginSchema.ts:
--------------------------------------------------------------------------------
1 | export interface LoginSchema {
2 | username: string;
3 | password: string;
4 | isLoading: boolean;
5 | error?: string;
6 | }
7 |
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_shared_Button_Background_Inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_shared_Button_Background_Inverted.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_iphone7_widget_Navbar_Aurhenticated_User.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_iphone7_widget_Navbar_Aurhenticated_User.png
--------------------------------------------------------------------------------
/.loki/reference/chrome_laptop_shared_Button_Background_Inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ushev-s/production_react/HEAD/.loki/reference/chrome_laptop_shared_Button_Background_Inverted.png
--------------------------------------------------------------------------------
/src/entities/Counter/model/selectors/getCounter/getCounter.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getCounter = (state: StateSchema) => state.counter;
4 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage/ui/NotFoundPage.module.scss:
--------------------------------------------------------------------------------
1 | .NotFoundPage {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | font: var(--font-l);
6 | height: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/src/entities/Counter/index.ts:
--------------------------------------------------------------------------------
1 | export { counterReducer } from './model/slice/counterSlice';
2 | export { Counter } from './ui/Counter';
3 | export type { CounterSchema } from './model/types/counterSchema';
4 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run build:prod
5 | npm run lint:ts
6 | npm run lint:scss
7 | npm run test:unit
8 | npm run storybook:build
9 | npm run test:ui
--------------------------------------------------------------------------------
/src/app/styles/reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | input,
8 | button,
9 | textarea,
10 | select {
11 | margin: 0;
12 | font: inherit;
13 | }
--------------------------------------------------------------------------------
/src/entities/User/model/selectors/getUserAuthData/getUserAuthData.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getUserAuthData = (state: StateSchema) => state.user.authData;
4 |
--------------------------------------------------------------------------------
/src/shared/config/storybook/StyleDecorator/StyleDecorator.ts:
--------------------------------------------------------------------------------
1 | import { Story } from '@storybook/react';
2 | import 'app/styles/index.scss';
3 |
4 | export const StyleDecorator = (story: () => Story) => story();
5 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-typescript",
5 | ["@babel/preset-react", { "runtime": "automatic" }]
6 | ],
7 | "plugins": ["i18next-extract"]
8 | }
9 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getLoginError = (state: StateSchema) => state?.loginForm?.error;
4 |
--------------------------------------------------------------------------------
/src/widgets/PageLoader/ui/PageLoader.module.scss:
--------------------------------------------------------------------------------
1 | .PageLoader {
2 | min-height: calc(100vh - var(--navbar-height));
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | flex-grow: 1;
7 | }
8 |
--------------------------------------------------------------------------------
/src/shared/lib/hooks/useAppDispatch/useAppDispatch.ts:
--------------------------------------------------------------------------------
1 | import { AppDispatch } from 'app/providers/StoreProvider';
2 | import { useDispatch } from 'react-redux';
3 |
4 | export const useAppDispatch = () => useDispatch();
5 |
--------------------------------------------------------------------------------
/src/app/providers/ThemeProvider/index.ts:
--------------------------------------------------------------------------------
1 | import ThemeProvider from './ui/ThemeProvider';
2 | import { useTheme } from './lib/useTheme';
3 | import { Theme } from './lib/ThemeContext';
4 |
5 | export { ThemeProvider, useTheme, Theme };
6 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getLoginPassword = (state: StateSchema) => state?.loginForm?.password ?? '';
4 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getLoginUsername = (state: StateSchema) => state?.loginForm?.username ?? '';
4 |
--------------------------------------------------------------------------------
/src/pages/MainPage/ui/MainPage.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 |
3 | const MainPage = () => {
4 | const { t } = useTranslation();
5 | return {t('Main page')}
;
6 | };
7 |
8 | export default MainPage;
9 |
--------------------------------------------------------------------------------
/src/app/providers/StoreProvider/index.ts:
--------------------------------------------------------------------------------
1 | export { StoreProvider } from './ui/StoreProvider';
2 | export { createReduxStore, AppDispatch } from './config/store';
3 | export type { StateSchema, ReduxStoreWithManager } from './config/StateSchema';
4 |
--------------------------------------------------------------------------------
/src/entities/User/index.ts:
--------------------------------------------------------------------------------
1 | export { userActions, userReducer } from './model/slice/userSlice';
2 | export { User, UserSchema } from './model/types/user';
3 | export { getUserAuthData } from './model/selectors/getUserAuthData/getUserAuthData';
4 |
--------------------------------------------------------------------------------
/src/widgets/PageError/ui/PageError.module.scss:
--------------------------------------------------------------------------------
1 | .PageError {
2 | width: 100%;
3 | height: 90vh;
4 | display: flex;
5 | flex-direction: column;
6 | row-gap: 30px;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.ts:
--------------------------------------------------------------------------------
1 | import { StateSchema } from 'app/providers/StoreProvider';
2 |
3 | export const getLoginIsLoading = (state: StateSchema) =>
4 | state?.loginForm?.isLoading ?? false;
5 |
--------------------------------------------------------------------------------
/src/app/styles/themes/light.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg-color: #e8e8ea;
3 | --inverted-bg-color: #10105f;
4 | --primary-color: #04268a;
5 | --secondary-color: #0449e0;
6 | --inverted-primary-color: #04ff04;
7 | --inverted-secondary-color: #119e11;
8 | }
9 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts:
--------------------------------------------------------------------------------
1 | import { FC, lazy } from 'react';
2 | import { LoginFormProps } from './LoginForm';
3 |
4 | export const LoginFormAsync = lazy>(
5 | async () => await import('./LoginForm')
6 | );
7 |
--------------------------------------------------------------------------------
/src/app/styles/themes/dark.scss:
--------------------------------------------------------------------------------
1 | .app_dark_theme {
2 | --bg-color: #10105f;
3 | --inverted-bg-color: #e8e8ea;
4 | --primary-color: #119e11;
5 | --secondary-color: #04ff04;
6 | --inverted-primary-color: #0449e0;
7 | --inverted-secondary-color: #04268a;
8 | }
9 |
--------------------------------------------------------------------------------
/src/shared/ui/AppLink/AppLink.module.scss:
--------------------------------------------------------------------------------
1 | .AppLink {
2 | color: var(--primary-color);
3 | }
4 |
5 | .primary {
6 | color: var(--primary-color);
7 | }
8 |
9 | .secondary {
10 | color: var(--inverted-primary-color);
11 | }
12 |
13 | .red {
14 | color: red;
15 | }
16 |
--------------------------------------------------------------------------------
/src/shared/config/storybook/RouterDecorator/RouterDecorator.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@storybook/react';
2 | import { BrowserRouter } from 'react-router-dom';
3 |
4 | export const RouterDecorator = (story: () => Story) => {
5 | return {story()};
6 | };
7 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/ui/LoginForm/LoginForm.module.scss:
--------------------------------------------------------------------------------
1 | .LoginForm {
2 | display: flex;
3 | flex-direction: column;
4 | width: 400px;
5 | }
6 |
7 | .input {
8 | margin-top: 10px;
9 | }
10 |
11 | .loginBtn {
12 | margin-top: 15px;
13 | margin-left: auto;
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/ui/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | const AboutPage = () => {
5 | const { t, i18n } = useTranslation('about');
6 | return {t('About page')}
;
7 | };
8 |
9 | export default AboutPage;
10 |
--------------------------------------------------------------------------------
/src/shared/const/common.ts:
--------------------------------------------------------------------------------
1 | export enum Currency {
2 | RUB = 'RUB',
3 | EUR = 'EUR',
4 | USD = 'USD'
5 | }
6 |
7 | export enum Country {
8 | Russia = 'Russia',
9 | Belarus = 'Belarus',
10 | Ukraine = 'Ukraine',
11 | Kazakhstan = 'Kazakhstan',
12 | Armenia = 'Armenia',
13 | US = 'US'
14 | }
15 |
--------------------------------------------------------------------------------
/src/widgets/Navbar/ui/Navbar.module.scss:
--------------------------------------------------------------------------------
1 | .Navbar {
2 | width: 100%;
3 | height: var(--navbar-height);
4 | background: var(--inverted-bg-color);
5 | display: flex;
6 | align-items: center;
7 | padding: 20px;
8 | }
9 |
10 | .links {
11 | margin-left: auto;
12 | }
13 |
14 | .mainLink {
15 | margin-right: 15px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/shared/ui/Text/Text.module.scss:
--------------------------------------------------------------------------------
1 | .title {
2 | font: var(--font-l);
3 | color: var(--primary-color);
4 | }
5 |
6 | .text {
7 | font: var(--font-m);
8 | color: var(--secondary-color);
9 | }
10 |
11 | .error {
12 | .title {
13 | color: var(--red-light);
14 | }
15 |
16 | .text {
17 | color: var(--red-dark);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/shared/ui/Portal/Portal.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { createPortal } from 'react-dom';
3 |
4 | interface PortalProps {
5 | children: ReactNode;
6 | element?: HTMLElement;
7 | }
8 |
9 | export const Portal = ({ children, element = document.body }: PortalProps) => {
10 | return createPortal(children, element);
11 | };
12 |
--------------------------------------------------------------------------------
/config/storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../../src/**/*.stories.@(js|jsx|ts|tsx)'],
3 | addons: [
4 | '@storybook/addon-links',
5 | '@storybook/addon-essentials',
6 | '@storybook/addon-interactions'
7 | ],
8 | framework: '@storybook/react',
9 | core: {
10 | builder: '@storybook/builder-webpack5'
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/config/build/buildDevServer.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions } from './types/config';
2 | // import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
3 |
4 | export function buildDevServer(options: BuildOptions) {
5 | return {
6 | port: options.port,
7 | open: true,
8 | historyApiFallback: true,
9 | hot: true
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/selectors/getCounterValue/getCounterValue.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from '@reduxjs/toolkit';
2 | import { CounterSchema } from '../../types/counterSchema';
3 | import { getCounter } from '../getCounter/getCounter';
4 |
5 | export const getCounterValue = createSelector(
6 | getCounter,
7 | (counter: CounterSchema) => counter.value
8 | );
9 |
--------------------------------------------------------------------------------
/config/build/buildResolvers.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions } from './types/config';
2 | import { ResolveOptions } from 'webpack';
3 |
4 | export function buildResolvers(options: BuildOptions): ResolveOptions {
5 | return {
6 | extensions: ['.tsx', '.ts', '.js'],
7 | preferAbsolute: true,
8 | modules: [options.paths.src, 'node_modules'],
9 | mainFiles: ['index'],
10 | alias: {}
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/config/i18n/i18nForTests.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 |
4 | i18n.use(initReactI18next).init({
5 | lng: 'en',
6 | fallbackLng: 'en',
7 | debug: false,
8 |
9 | interpolation: {
10 | escapeValue: false // not needed for react!!
11 | },
12 |
13 | resources: { en: { translations: {} } }
14 | });
15 |
16 | export default i18n;
17 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.module.scss:
--------------------------------------------------------------------------------
1 | .item {
2 | display: flex;
3 | align-items: center;
4 | margin-top: 10px;
5 | }
6 |
7 | .link {
8 | margin-left: 10px;
9 | opacity: 1;
10 | }
11 |
12 | .icon {
13 | fill: var(--inverted-primary-color);
14 | }
15 |
16 | .collapsed {
17 | .link {
18 | opacity: 0;
19 | transition: 0.2s opacity;
20 | width: 0;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/shared/lib/classNames/classNames.ts:
--------------------------------------------------------------------------------
1 | type Mods = Record
2 |
3 | export function classNames(
4 | cls: string,
5 | mods: Mods,
6 | additional: string[]
7 | ): string {
8 | return [
9 | cls,
10 | ...additional.filter(Boolean),
11 | ...Object.entries(mods)
12 | .filter(([cls, value]) => Boolean(value))
13 | .map(([cls, value]) => cls)
14 | ].join(' ');
15 | }
16 |
--------------------------------------------------------------------------------
/src/shared/lib/tests/renderWithTranslation/renderWithTranslation.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { ReactNode } from 'react';
3 | import { I18nextProvider } from 'react-i18next';
4 | import i18nForTests from 'shared/config/i18n/i18nForTests';
5 |
6 | export function renderWithTranslation(component: ReactNode) {
7 | return render({component});
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import 'reset';
2 | @import 'variables/global';
3 | @import 'themes/light';
4 | @import 'themes/dark';
5 |
6 | body {
7 | font: var(--font-m);
8 | color: var(--primary-color);
9 | }
10 |
11 | .app {
12 | background: var(--bg-color);
13 | min-height: 100vh;
14 | }
15 |
16 | .content-page {
17 | display: flex;
18 | }
19 |
20 | .page-wrapper {
21 | flex-grow: 1;
22 | padding: 20px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/shared/ui/Loader/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from 'shared/lib/classNames/classNames';
2 | import './Loader.scss';
3 |
4 | interface LoaderProps {
5 | className?: string;
6 | }
7 |
8 | export const Loader = ({ className = '' }: LoaderProps) => {
9 | return (
10 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/config/build/types/config.ts:
--------------------------------------------------------------------------------
1 | export type BuildMode = 'production' | 'development';
2 |
3 | export interface BuildPaths {
4 | entry: string;
5 | build: string;
6 | html: string;
7 | src: string;
8 | }
9 |
10 | export interface BuildEnv {
11 | mode: BuildMode;
12 | port: number;
13 | }
14 |
15 | export interface BuildOptions {
16 | mode: BuildMode;
17 | paths: BuildPaths;
18 | isDev: boolean;
19 | port: number;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/providers/ThemeProvider/lib/ThemeContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export enum Theme {
4 | LIGHT = 'app_light_theme',
5 | DARK = 'app_dark_theme'
6 | }
7 |
8 | export interface ThemeContextProps {
9 | theme?: Theme;
10 | setTheme?: (theme: Theme) => void;
11 | }
12 |
13 | export const ThemeContext = createContext({});
14 |
15 | export const LOCAL_STORAGE_THEME_KEY = 'theme';
16 |
--------------------------------------------------------------------------------
/src/shared/config/storybook/ThemeDecorator/ThemeDecorator.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@storybook/react';
2 | import { Theme, ThemeProvider } from 'app/providers/ThemeProvider';
3 | // test
4 | export const ThemeDecorator = (theme: Theme) => (StoryComponent: Story) => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/entities/Profile/model/types/profile.ts:
--------------------------------------------------------------------------------
1 | import { Country, Currency } from 'shared/const/common';
2 |
3 | export interface Profile {
4 | first: string;
5 | lastname: string;
6 | age: 25;
7 | currency: Currency;
8 | country: Country;
9 | city: string;
10 | username: string;
11 | avatar: string;
12 | }
13 |
14 | export interface ProfileSchema {
15 | data?: Profile;
16 | isLoading: boolean;
17 | error?: string;
18 | readonly: boolean;
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/providers/ErrorBoundary/ui/BugButton.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Button } from 'shared/ui/Button/Button';
3 |
4 | // Testing component
5 | export const BugButton = () => {
6 | const [error, setError] = useState(false);
7 |
8 | const throwError = () => setError(true);
9 |
10 | useEffect(() => {
11 | if (error) throw new Error();
12 | }, [error]);
13 |
14 | return ;
15 | };
16 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Language": "English",
3 | "Page Not found": "Page Not found",
4 | "An unexpected error occurred": "An unexpected error occurred",
5 | "Reload page": "Reload page",
6 | "Short Language": "En",
7 | "Login": "Login",
8 | "Enter username": "Enter username",
9 | "Enter password": "Enter password",
10 | "Login Form": "Login Form",
11 | "Incorrect username or password": "Incorrect username or password",
12 | "Logout": "Logout"
13 | }
14 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/selectors/getCounterValue/getCounterValue.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getCounterValue } from './getCounterValue';
4 |
5 | describe('getCounterValue.test', () => {
6 | test('', () => {
7 | const state: DeepPartial = {
8 | counter: { value: 10 }
9 | };
10 | expect(getCounterValue(state as StateSchema)).toEqual(10);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/widgets/PageLoader/ui/PageLoader.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from 'shared/lib/classNames/classNames';
2 | import { Loader } from 'shared/ui/Loader/Loader';
3 | import cls from './PageLoader.module.scss';
4 |
5 | interface PageLoaderProps {
6 | className?: string;
7 | }
8 |
9 | export const PageLoader = ({ className = '' }: PageLoaderProps) => {
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/selectors/getCounter/getCounter.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getCounter } from './getCounter';
4 |
5 | describe('getCounter', () => {
6 | test('should return counter value', () => {
7 | const state: DeepPartial = {
8 | counter: { value: 10 }
9 | };
10 | expect(getCounter(state as StateSchema)).toEqual({ value: 10 });
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/public/locales/ru/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Language": "Русский",
3 | "Page Not found": "Страница не найдена",
4 | "An unexpected error occurred": "Произошла непредвиденная ошибка",
5 | "Reload page": "Обновить страницу",
6 | "Short Language": "Ru",
7 | "Login": "Войти",
8 | "Enter username": "Введите логин",
9 | "Enter password": "Введите пароль",
10 | "Login Form": "Форма авторизации",
11 | "Incorrect username or password": "Неверное имя пользователя или пароль",
12 | "Logout": "Выйти"
13 | }
14 |
--------------------------------------------------------------------------------
/extractedTranslations/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "About page",
3 | "An unexpected error occurred": "An unexpected error occurred",
4 | "Enter password": "Enter password",
5 | "Enter username": "Enter username",
6 | "Incorrect username or password": "",
7 | "Language": "Language",
8 | "Login": "Login",
9 | "Login Form": "",
10 | "Logout": "",
11 | "Main page": "Main page",
12 | "PROFILE PAGE": "PROFILE PAGE",
13 | "Page Not found": "Page Not found",
14 | "Reload page": "Reload page",
15 | "Switch 2": ""
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage/ui/NotFoundPage.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import cls from './NotFoundPage.module.scss';
4 |
5 | interface NotFoundPageProps {
6 | className?: string;
7 | }
8 |
9 | export const NotFoundPage = ({ className = '' }: NotFoundPageProps) => {
10 | const { t } = useTranslation();
11 | return (
12 |
13 | {t('Page Not found')}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/shared/ui/Input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentStory, ComponentMeta } from '@storybook/react';
2 | import { Input } from './Input';
3 |
4 | export default {
5 | title: 'shared/Input',
6 | component: Input,
7 | argTypes: {
8 | backgroundColor: { control: 'color' }
9 | }
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | export const Primary = Template.bind({});
15 | Primary.args = {
16 | placeholder: 'Type text',
17 | value: '123121'
18 | };
19 |
--------------------------------------------------------------------------------
/src/shared/ui/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { Button, ButtonTheme } from 'shared/ui/Button/Button';
3 |
4 | describe('Button', () => {
5 | test('Test render', () => {
6 | render();
7 | expect(screen.getByText('TEST')).toBeInTheDocument();
8 | });
9 |
10 | test('Test clear theme', () => {
11 | render();
12 | expect(screen.getByText('TEST')).toHaveClass('clear');
13 | screen.debug();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/extractedTranslations/ru/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "About page": "About page",
3 | "An unexpected error occurred": "An unexpected error occurred",
4 | "Enter password": "Enter password",
5 | "Enter username": "Enter username",
6 | "Incorrect username or password": "Incorrect username or password",
7 | "Language": "Language",
8 | "Login": "Login",
9 | "Login Form": "Login Form",
10 | "Logout": "Logout",
11 | "Main page": "Main page",
12 | "PROFILE PAGE": "PROFILE PAGE",
13 | "Page Not found": "Page Not found",
14 | "Reload page": "Reload page"
15 | }
16 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/slice/counterSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { CounterSchema } from '../types/counterSchema';
3 |
4 | const initialState: CounterSchema = {
5 | value: 0
6 | };
7 |
8 | export const counterSlice = createSlice({
9 | name: 'counter',
10 | initialState,
11 | reducers: {
12 | increment: (state) => {
13 | state.value += 1;
14 | },
15 | decrement: (state) => {
16 | state.value -= 1;
17 | }
18 | }
19 | });
20 |
21 | export const { actions: counterActions } = counterSlice;
22 | export const { reducer: counterReducer } = counterSlice;
23 |
--------------------------------------------------------------------------------
/src/entities/Profile/model/slice/profileSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { USER_LOCALSTORAGE_KEY } from 'shared/const/localstorage';
3 | import { Profile, ProfileSchema } from '../types/profile';
4 |
5 | const initialState: ProfileSchema = {
6 | readonly: true,
7 | isLoading: false,
8 | error: undefined,
9 | data: undefined
10 | };
11 |
12 | export const profileSlice = createSlice({
13 | name: 'profile',
14 | initialState,
15 | reducers: {}
16 | });
17 |
18 | export const { actions: profileActions } = profileSlice;
19 | export const { reducer: profileReducer } = profileSlice;
20 |
--------------------------------------------------------------------------------
/src/shared/config/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 |
4 | import Backend from 'i18next-http-backend';
5 | import LanguageDetector from 'i18next-browser-languagedetector';
6 |
7 | i18n
8 | .use(Backend)
9 | .use(LanguageDetector)
10 | .use(initReactI18next)
11 | .init({
12 | fallbackLng: 'en',
13 | debug: __IS_DEV__,
14 |
15 | interpolation: {
16 | escapeValue: false // not needed for react as it escapes by default
17 | },
18 |
19 | backend: {
20 | loadPath: '/locales/{{lng}}/{{ns}}.json'
21 | }
22 | });
23 |
24 | export default i18n;
25 |
--------------------------------------------------------------------------------
/src/app/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.scss' {
2 | interface IClassNames {
3 | [className: string]: string;
4 | }
5 | const classNames: IClassNames;
6 | export = classNames;
7 | }
8 |
9 | declare module '*.css' {
10 | interface IClassNames {
11 | [className: string]: string;
12 | }
13 | const classNames: IClassNames;
14 | export = classNames;
15 | }
16 |
17 | declare module '*.png';
18 | declare module '*.jpg';
19 | declare module '*.jpeg';
20 | declare module '*.svg' {
21 | import React from 'react';
22 | const SVG: React.VFC>;
23 | export default SVG;
24 | }
25 |
26 | declare const __IS_DEV__: boolean;
27 |
--------------------------------------------------------------------------------
/config/build/loaders/buildCssLoader.ts:
--------------------------------------------------------------------------------
1 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
2 |
3 | export function buildCssLoader(isDev: boolean) {
4 | return {
5 | test: /\.s[ac]ss$/i,
6 | use: [
7 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
8 | {
9 | loader: 'css-loader',
10 | options: {
11 | modules: {
12 | auto: (resPath: string) => Boolean(resPath.includes('.module.')),
13 | localIdentName: isDev
14 | ? '[path][name]__[local]--[hash:base64:5]'
15 | : '[hash:base64:8]'
16 | }
17 | }
18 | },
19 | 'sass-loader'
20 | ]
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/providers/ThemeProvider/lib/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { LOCAL_STORAGE_THEME_KEY, Theme, ThemeContext } from './ThemeContext';
3 |
4 | interface UseThemeResult {
5 | toggleTheme: () => void;
6 | theme: Theme | undefined;
7 | }
8 |
9 | export function useTheme(): UseThemeResult {
10 | const { theme, setTheme } = useContext(ThemeContext);
11 |
12 | const toggleTheme = () => {
13 | const newTheme = theme === Theme.DARK ? Theme.LIGHT : Theme.DARK;
14 | setTheme?.(newTheme);
15 | document.body.className = newTheme;
16 | localStorage.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
17 | };
18 |
19 | return { theme, toggleTheme };
20 | }
21 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBoundary } from 'app/providers/ErrorBoundary';
2 | import { ThemeProvider } from 'app/providers/ThemeProvider';
3 | import { render } from 'react-dom';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { StoreProvider } from 'app/providers/StoreProvider';
6 | import 'shared/config/i18n/i18n';
7 | import 'app/styles/index.scss';
8 | import App from './app/App';
9 |
10 | render(
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ,
20 | document.getElementById('root')
21 | );
22 |
--------------------------------------------------------------------------------
/src/app/providers/router/ui/AppRouter.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { Route, Routes } from 'react-router-dom';
3 | import { routeConfig } from 'shared/config/routeConfig/routeConfig';
4 | import { PageLoader } from 'widgets/PageLoader';
5 |
6 | const AppRouter = () => {
7 | return (
8 | }>
9 |
10 | {Object.values(routeConfig).map(({ element, path }) => (
11 | {element}}
14 | path={path}
15 | />
16 | ))}
17 |
18 |
19 | );
20 | };
21 |
22 | export default AppRouter;
23 |
--------------------------------------------------------------------------------
/src/widgets/PageError/ui/PageError.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import { Button } from 'shared/ui/Button/Button';
4 | import cls from './PageError.module.scss';
5 |
6 | interface PageErrorProps {
7 | className?: string;
8 | }
9 |
10 | export const PageError = ({ className = '' }: PageErrorProps) => {
11 | const { t } = useTranslation();
12 |
13 | const reloadPage = () => {
14 | location.reload();
15 | };
16 |
17 | return (
18 |
19 |
{t('An unexpected error occurred')}
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/app/providers/ThemeProvider/ui/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo, useState } from 'react';
2 | import { LOCAL_STORAGE_THEME_KEY, Theme, ThemeContext } from '../lib/ThemeContext';
3 |
4 | const defaultTheme =
5 | (localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as Theme) || Theme.LIGHT;
6 |
7 | interface ThemeProviderProps {
8 | initialTheme?: Theme;
9 | }
10 |
11 | const ThemeProvider: FC = ({ children, initialTheme }) => {
12 | const [theme, setTheme] = useState(initialTheme ?? defaultTheme);
13 |
14 | const defaultProps = useMemo(() => ({ theme, setTheme }), [theme]);
15 | return {children};
16 | };
17 |
18 | export default ThemeProvider;
19 |
--------------------------------------------------------------------------------
/src/entities/Counter/model/slice/counterSlice.test.ts:
--------------------------------------------------------------------------------
1 | import { CounterSchema } from '../types/counterSchema';
2 | import { counterReducer, counterActions } from './counterSlice';
3 |
4 | describe('counterSlice.test', () => {
5 | test('decrement', () => {
6 | const state: CounterSchema = { value: 10 };
7 | expect(counterReducer(state, counterActions.decrement)).toEqual({ value: 9 });
8 | });
9 |
10 | test('increment', () => {
11 | const state: CounterSchema = { value: 10 };
12 | expect(counterReducer(state, counterActions.increment)).toEqual({ value: 11 });
13 | });
14 |
15 | test('should work with empty state', () => {
16 | expect(counterReducer(undefined, counterActions.increment)).toEqual({ value: 1 });
17 | });
18 | });
19 | // 29:05
20 |
--------------------------------------------------------------------------------
/src/shared/ui/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import cls from './Text.module.scss';
4 |
5 | export enum TextTheme {
6 | PRIMARY = 'primary',
7 | ERROR = 'error'
8 | }
9 |
10 | interface TextProps {
11 | className?: string;
12 | title?: string;
13 | text?: string;
14 | theme?: TextTheme;
15 | }
16 |
17 | export const Text = memo(
18 | ({ className = '', title, text, theme = TextTheme.PRIMARY }: TextProps) => {
19 | return (
20 |
21 | {title &&
{title}
}
22 | {text &&
{text}
}
23 |
24 | );
25 | }
26 | );
27 |
--------------------------------------------------------------------------------
/config/storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { addDecorator } from '@storybook/react';
2 | import { StyleDecorator } from '../../src/shared/config/storybook/StyleDecorator/StyleDecorator';
3 | import { ThemeDecorator } from '../../src/shared/config/storybook/ThemeDecorator/ThemeDecorator';
4 | import { RouterDecorator } from '../../src/shared/config/storybook/RouterDecorator/RouterDecorator';
5 | import { Theme } from '../../src/app/providers/ThemeProvider';
6 |
7 | export const parameters = {
8 | actions: { argTypesRegex: '^on[A-Z].*' },
9 | controls: {
10 | matchers: {
11 | color: /(background|color)$/i,
12 | date: /Date$/
13 | }
14 | }
15 | };
16 |
17 | addDecorator(StyleDecorator);
18 | addDecorator(ThemeDecorator(Theme.LIGHT));
19 | addDecorator(RouterDecorator);
20 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/Sidebar/Sidebar.test.tsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, screen } from '@testing-library/react';
2 | import { componentRender } from 'shared/lib/tests/componentRender/componentRender';
3 | import { Sidebar } from 'widgets/Sidebar/ui/Sidebar/Sidebar';
4 |
5 | describe('Sidebar', () => {
6 | test('with first param', () => {
7 | componentRender();
8 | expect(screen.getByTestId('sidebar')).toBeInTheDocument();
9 | });
10 |
11 | test('test toggle', () => {
12 | componentRender();
13 | const toggleBtn = screen.getByTestId('sidebar-toggle');
14 | expect(screen.getByTestId('sidebar')).toBeInTheDocument();
15 | fireEvent.click(toggleBtn);
16 | expect(screen.getByTestId('sidebar')).toHaveClass('collapsed');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getLoginError } from './getLoginError';
4 |
5 | describe('getLoginError.test', () => {
6 | test('should return error', () => {
7 | const state: DeepPartial = {
8 | loginForm: {
9 | username: '',
10 | password: '',
11 | isLoading: false,
12 | error: 'error'
13 | }
14 | };
15 | expect(getLoginError(state as StateSchema)).toEqual('error');
16 | });
17 | test('should work with empty state', () => {
18 | const state: DeepPartial = {};
19 | expect(getLoginError(state as StateSchema)).toEqual(undefined);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/pages/MainPage/ui/MainPage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import MainPage from './MainPage';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'pages/MainPage',
10 | component: MainPage,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Normal = Template.bind({});
19 | Normal.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/shared/ui/Loader/Loader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { Loader } from './Loader';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'shared/Loader',
10 | component: Loader,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Normal = Template.bind({});
19 | Normal.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/pages/AboutPage/ui/AboutPage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import AboutPage from './AboutPage';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'pages/AboutPage',
10 | component: AboutPage,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Normal = Template.bind({});
19 | Normal.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/model/items.ts:
--------------------------------------------------------------------------------
1 | import { RoutePath } from 'shared/config/routeConfig/routeConfig';
2 | import AboutIcon from 'shared/assets/icons/about-20-20.svg';
3 | import MainIcon from 'shared/assets/icons/main-20-20.svg';
4 | import ProfileIcon from 'shared/assets/icons/profile-20-20.svg';
5 |
6 | export interface SidebarItemType {
7 | path: string;
8 | text: string;
9 | Icon: React.VFC>;
10 | }
11 |
12 | export const SidebarItemsList: SidebarItemType[] = [
13 | {
14 | path: RoutePath.main,
15 | Icon: MainIcon,
16 | text: 'Main page'
17 | },
18 | {
19 | path: RoutePath.about,
20 | Icon: AboutIcon,
21 | text: 'About page'
22 | },
23 | {
24 | path: RoutePath.profile,
25 | Icon: ProfileIcon,
26 | text: 'Profile page'
27 | }
28 | ];
29 |
--------------------------------------------------------------------------------
/json-server/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "posts": [
3 | {
4 | "id": "1",
5 | "title": "json-server",
6 | "userId": "1"
7 | },
8 | {
9 | "id": "2",
10 | "title": "json-server",
11 | "userId": "2"
12 | }
13 | ],
14 | "comments": [
15 | {
16 | "id": "1",
17 | "body": "some comment",
18 | "postId": "1"
19 | }
20 | ],
21 | "users": [
22 | {
23 | "id": "1",
24 | "username": "admin",
25 | "password": "123"
26 | }
27 | ],
28 | "profile": {
29 | "first": "Test",
30 | "lastname": "User",
31 | "age": 25,
32 | "currency": "USD",
33 | "country": "US",
34 | "city": "Arlington",
35 | "username": "admin",
36 | "avatar": "https://pic.rutubelist.ru/user/3b/27/3b2758ad5492a76b578f7ee072e4e894.jpg"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginIsLoading/getLoginIsLoading.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getLoginIsLoading } from './getLoginIsLoading';
4 |
5 | describe('getLoginIsLoading.test', () => {
6 | test('should return value', () => {
7 | const state: DeepPartial = {
8 | loginForm: {
9 | username: '',
10 | password: '',
11 | isLoading: true,
12 | error: ''
13 | }
14 | };
15 | expect(getLoginIsLoading(state as StateSchema)).toEqual(true);
16 | });
17 | test('should work with empty state', () => {
18 | const state: DeepPartial = {};
19 | expect(getLoginIsLoading(state as StateSchema)).toEqual(false);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getLoginPassword } from './getLoginPassword';
4 |
5 | describe('getLoginPassword.test', () => {
6 | test('should return value', () => {
7 | const state: DeepPartial = {
8 | loginForm: {
9 | username: '',
10 | password: '123123',
11 | isLoading: false,
12 | error: ''
13 | }
14 | };
15 | expect(getLoginPassword(state as StateSchema)).toEqual('123123');
16 | });
17 | test('should work with empty state', () => {
18 | const state: DeepPartial = {};
19 | expect(getLoginPassword(state as StateSchema)).toEqual('');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 | import { getLoginUsername } from './getLoginUsername';
4 |
5 | describe('getLoginUsername.test', () => {
6 | test('should return value', () => {
7 | const state: DeepPartial = {
8 | loginForm: {
9 | username: 'admin',
10 | password: '',
11 | isLoading: false,
12 | error: ''
13 | }
14 | };
15 | expect(getLoginUsername(state as StateSchema)).toEqual('admin');
16 | });
17 | test('should work with empty state', () => {
18 | const state: DeepPartial = {};
19 | expect(getLoginUsername(state as StateSchema)).toEqual('');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/Sidebar/Sidebar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
5 | import { Theme } from 'app/providers/ThemeProvider';
6 | import { Sidebar } from './Sidebar';
7 |
8 | export default {
9 | title: 'widget/Sidebar',
10 | component: Sidebar,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Light = Template.bind({});
19 | Light.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/widgets/PageError/ui/PageError.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
5 | import { Theme } from 'app/providers/ThemeProvider';
6 | import { PageError } from './PageError';
7 |
8 | export default {
9 | title: 'widget/PageError',
10 | component: PageError,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Light = Template.bind({});
19 | Light.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/app/providers/StoreProvider/ui/StoreProvider.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit';
2 | import { ReactNode } from 'react';
3 | import { Provider } from 'react-redux';
4 | import { StateSchema } from '../config/StateSchema';
5 | import { createReduxStore } from '../config/store';
6 |
7 | interface StoreProviderProps {
8 | children?: ReactNode;
9 | initialState?: DeepPartial;
10 | asyncReducers?: DeepPartial>;
11 | }
12 |
13 | export const StoreProvider = ({
14 | children,
15 | initialState,
16 | asyncReducers
17 | }: StoreProviderProps) => {
18 | const store = createReduxStore(
19 | initialState as StateSchema,
20 | asyncReducers as ReducersMapObject
21 | );
22 | return {children};
23 | };
24 |
--------------------------------------------------------------------------------
/src/app/styles/variables/global.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-family-main: Consolas, 'Times New Roman', Serif;
3 |
4 | --font-size-m: 16px;
5 | --font-line-m: 24px;
6 | --font-m: var(--font-size-m) / var(--font-line-m) var(--font-family-main);
7 |
8 | --font-size-l: 24px;
9 | --font-line-l: 32px;
10 | --font-l: var(--font-size-l) / var(--font-line-l) var(--font-family-main);
11 |
12 | --font-size-xl: 32px;
13 | --font-line-xl: 40px;
14 | --font-xl: var(--font-size-xl) / var(--font-line-xl) var(--font-family-main);
15 |
16 | //Dimensions
17 | --navbar-height: 50px;
18 |
19 | --sidebar-width: 300px;
20 | --sidebar-width-collapsed: 80px;
21 |
22 | // z-indexes
23 | --modal-z-index: 10;
24 |
25 | //colors
26 | --overlay-color: rgba(0 0 0 / 60%);
27 | --red-light: #ff0000;
28 | --red-dark: #ce0505;
29 | }
30 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 | import { buildWebpackConfig } from './config/build/buildWebpackConfig';
4 | import { BuildEnv, BuildPaths } from './config/build/types/config';
5 |
6 | export default (env: BuildEnv) => {
7 | const paths: BuildPaths = {
8 | entry: path.resolve(__dirname, 'src', 'index.tsx'),
9 | build: path.resolve(__dirname, 'build'),
10 | html: path.resolve(__dirname, 'public', 'index.html'),
11 | src: path.resolve(__dirname, 'src')
12 | };
13 |
14 | const mode = env.mode || 'development';
15 | const PORT = env.port || 3000;
16 | const isDev = mode === 'development';
17 |
18 | const config: webpack.Configuration = buildWebpackConfig({
19 | mode,
20 | paths,
21 | isDev,
22 | port: PORT
23 | });
24 |
25 | return config;
26 | };
27 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/slice/loginSlice.test.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from '@reduxjs/toolkit';
2 | import { LoginSchema } from '../types/loginSchema';
3 | import { loginActions, loginReducer } from './loginSlice';
4 |
5 | describe('loginSlice.test', () => {
6 | test('test set username', async () => {
7 | const state: DeepPartial = { username: '123' };
8 | expect(
9 | loginReducer(state as LoginSchema, loginActions.setUsername('123123'))
10 | ).toEqual({
11 | username: '123123'
12 | });
13 | });
14 |
15 | test('test set password', async () => {
16 | const state: DeepPartial = { password: '123' };
17 | expect(
18 | loginReducer(state as LoginSchema, loginActions.setPassword('123123'))
19 | ).toEqual({
20 | password: '123123'
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage/ui/NotFoundPage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { NotFoundPage } from './NotFoundPage';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'pages/NotFoundPage',
10 | component: NotFoundPage,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Normal = Template.bind({});
19 | Normal.args = {};
20 |
21 | export const Dark = Template.bind({});
22 | Dark.args = {};
23 |
24 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
25 |
--------------------------------------------------------------------------------
/src/shared/ui/Input/Input.module.scss:
--------------------------------------------------------------------------------
1 | .InputWrapper {
2 | display: flex;
3 | }
4 |
5 | .placeholder {
6 | margin-right: 5px;
7 | }
8 |
9 | .input {
10 | background: transparent;
11 | border: none;
12 | outline: none;
13 | width: 100%;
14 | color: transparent;
15 | text-shadow: 0 0 0 var(--primary-color);
16 |
17 | &:focus {
18 | outline: none;
19 | }
20 | }
21 |
22 | .caretWrapper {
23 | flex-grow: 1;
24 | position: relative;
25 | }
26 |
27 | .caret {
28 | position: absolute;
29 | height: 3px;
30 | width: 9px;
31 | background: var(--primary-color);
32 | bottom: 0;
33 | left: 0;
34 | animation: blink 0.7s forwards infinite;
35 | }
36 |
37 | @keyframes blink {
38 | 0% {
39 | opacity: 0;
40 | }
41 |
42 | 50% {
43 | opacity: 0.1;
44 | }
45 |
46 | 100% {
47 | opacity: 1;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/widgets/LangSwitcher/LangSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from 'shared/lib/classNames/classNames';
2 | import { useTranslation } from 'react-i18next';
3 | import { Button, ButtonTheme } from 'shared/ui/Button/Button';
4 | import { memo } from 'react';
5 |
6 | interface LangSwitcherProps {
7 | className?: string;
8 | short?: boolean;
9 | }
10 |
11 | export const LangSwitcher = memo(({ className = '', short }: LangSwitcherProps) => {
12 | const { t, i18n } = useTranslation();
13 | const toggle = () => {
14 | i18n.changeLanguage(i18n.language === 'ru' ? 'en' : 'ru');
15 | };
16 |
17 | return (
18 |
25 | );
26 | });
27 |
--------------------------------------------------------------------------------
/src/pages/ProfilePage/ui/ProfilePage.tsx:
--------------------------------------------------------------------------------
1 | import { profileReducer } from 'entities/Profile';
2 | import { useTranslation } from 'react-i18next';
3 | import { classNames } from 'shared/lib/classNames/classNames';
4 | import {
5 | DynamicModuleLoader,
6 | ReducersList
7 | } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
8 |
9 | const reducers: ReducersList = {
10 | profile: profileReducer
11 | };
12 |
13 | interface ProfilePageProps {
14 | className?: string;
15 | }
16 |
17 | const ProfilePage = ({ className = '' }: ProfilePageProps) => {
18 | const { t } = useTranslation();
19 | return (
20 |
21 | {t('PROFILE PAGE')}
22 |
23 | );
24 | };
25 |
26 | export default ProfilePage;
27 |
--------------------------------------------------------------------------------
/src/widgets/ThemeSwitcher/ui/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import { Theme, useTheme } from 'app/providers/ThemeProvider';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import LightIcon from 'shared/assets/icons/theme-light.svg';
4 | import DarkIcon from 'shared/assets/icons/theme-dark.svg';
5 | import { Button, ButtonTheme } from 'shared/ui/Button/Button';
6 | import { memo } from 'react';
7 |
8 | interface ThemeSwitcherProps {
9 | className?: string;
10 | }
11 |
12 | export const ThemeSwitcher = memo(({ className = '' }: ThemeSwitcherProps) => {
13 | const { theme, toggleTheme } = useTheme();
14 | return (
15 |
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/src/shared/ui/Modal/Modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentStory, ComponentMeta } from '@storybook/react';
2 | import { Theme } from 'app/providers/ThemeProvider';
3 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
4 | import { Modal } from './Modal';
5 |
6 | export default {
7 | title: 'shared/Modal',
8 | component: Modal,
9 | argTypes: {
10 | backgroundColor: { control: 'color' }
11 | }
12 | } as ComponentMeta;
13 |
14 | const Template: ComponentStory = (args) => ;
15 |
16 | export const Primary = Template.bind({});
17 | Primary.args = {
18 | isOpen: true,
19 | children: '.....______.......'
20 | };
21 |
22 | export const Dark = Template.bind({});
23 | Dark.args = {
24 | isOpen: true,
25 | children: '.....______.......'
26 | };
27 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
28 |
--------------------------------------------------------------------------------
/src/widgets/ThemeSwitcher/ui/ThemeSwitcher.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { ThemeSwitcher } from './ThemeSwitcher';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'widgets/ThemeSwitcher',
10 | component: ThemeSwitcher,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => (
17 |
18 | );
19 |
20 | export const Normal = Template.bind({});
21 | Normal.args = {};
22 |
23 | export const Dark = Template.bind({});
24 | Dark.args = {};
25 |
26 | Dark.decorators = [ThemeDecorator(Theme.DARK)];
27 |
--------------------------------------------------------------------------------
/src/shared/ui/Modal/Modal.module.scss:
--------------------------------------------------------------------------------
1 | .Modal {
2 | position: fixed;
3 | top: 0;
4 | bottom: 0;
5 | right: 0;
6 | left: 0;
7 | z-index: -1;
8 | opacity: 0;
9 | pointer-events: none;
10 | color: var(--primary-color);
11 | }
12 |
13 | .overlay {
14 | width: 100%;
15 | height: 100%;
16 | background-color: var(--overlay-color);
17 | display: flex;
18 | align-items: center;
19 | justify-content: center;
20 | }
21 |
22 | .content {
23 | padding: 20px;
24 | border-radius: 12px;
25 | background: var(--bg-color);
26 | transition: 0.3s transform;
27 | transform: scale(0.5);
28 | max-width: 60%;
29 | }
30 |
31 | .opened {
32 | pointer-events: auto;
33 | opacity: 1;
34 | z-index: var(--modal-z-index);
35 |
36 | .content {
37 | transform: scale(1);
38 | }
39 | }
40 |
41 | .isClosing {
42 | .content {
43 | transform: scale(0.2);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss:
--------------------------------------------------------------------------------
1 | .Sidebar {
2 | height: calc(100vh - var(--navbar-height));
3 | width: var(--sidebar-width);
4 | background: var(--inverted-bg-color);
5 | position: relative;
6 | transition: width 0.3s;
7 | }
8 |
9 | .switchers {
10 | position: absolute;
11 | bottom: 20px;
12 | display: flex;
13 | justify-content: center;
14 | width: 100%;
15 | }
16 |
17 | .lang {
18 | margin-left: 20px;
19 | }
20 |
21 | .collapseBtn {
22 | position: absolute;
23 | right: -32px;
24 | bottom: 32px;
25 | }
26 |
27 | .items {
28 | margin-top: 20px;
29 | margin-left: 30px;
30 | display: flex;
31 | flex-direction: column;
32 | }
33 |
34 | .collapsed {
35 | width: var(--sidebar-width-collapsed);
36 |
37 | .switchers {
38 | flex-direction: column;
39 | align-items: center;
40 | }
41 |
42 | .lang {
43 | margin-left: 0;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import { Loader } from 'shared/ui/Loader/Loader';
4 | import { Modal } from 'shared/ui/Modal/Modal';
5 | import { LoginFormAsync } from '../LoginForm/LoginForm.async';
6 | // import cls from './LoginModal.module.scss';
7 |
8 | interface LoginModalProps {
9 | className?: string;
10 | isOpen: boolean;
11 | onClose: () => void;
12 | }
13 |
14 | export const LoginModal = ({ className = '', isOpen, onClose }: LoginModalProps) => {
15 | return (
16 |
22 | }>
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/app/App.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from 'shared/lib/classNames/classNames';
2 | // import { useTheme } from 'app/providers/ThemeProvider';
3 | import { AppRouter } from 'app/providers/router';
4 | import { Navbar } from 'widgets/Navbar';
5 | import { Sidebar } from 'widgets/Sidebar';
6 | import { Suspense, useEffect } from 'react';
7 | import { useDispatch } from 'react-redux';
8 | import { userActions } from 'entities/User';
9 |
10 | const App = () => {
11 | const dispatch = useDispatch();
12 |
13 | useEffect(() => {
14 | dispatch(userActions.initAuthData());
15 | }, [dispatch]);
16 |
17 | return (
18 |
27 | );
28 | };
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { classNames } from 'shared/lib/classNames/classNames';
4 | import { AppLink, AppLinkTheme } from 'shared/ui/AppLink/AppLink';
5 | import { SidebarItemType } from '../../model/items';
6 | import cls from './SidebarItem.module.scss';
7 |
8 | interface SidebarItemProps {
9 | item: SidebarItemType;
10 | collapsed: boolean;
11 | }
12 |
13 | export const SidebarItem = memo(({ item, collapsed }: SidebarItemProps) => {
14 | const { t } = useTranslation();
15 | return (
16 |
21 |
22 | {t(item.text)}
23 |
24 | );
25 | });
26 |
--------------------------------------------------------------------------------
/src/shared/lib/tests/TestAsyncThunk/TestAsyncThunk.ts:
--------------------------------------------------------------------------------
1 | import { AsyncThunkAction } from '@reduxjs/toolkit';
2 | import { StateSchema } from 'app/providers/StoreProvider';
3 |
4 | type ActionCreatorType = (
5 | arg: Arg
6 | ) => AsyncThunkAction;
7 |
8 | export class TestAsyncThunk {
9 | dispatch: jest.MockedFn;
10 | getState: () => StateSchema;
11 | actionCreator: ActionCreatorType;
12 |
13 | constructor(actionCreator: ActionCreatorType) {
14 | this.actionCreator = actionCreator;
15 | this.dispatch = jest.fn();
16 | this.getState = jest.fn();
17 | }
18 |
19 | async callThunk(arg: Arg) {
20 | const action = this.actionCreator(arg);
21 | const result = await action(this.dispatch, this.getState, undefined);
22 | return result;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/shared/ui/AppLink/AppLink.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo, ReactNode } from 'react';
2 | import { Link, LinkProps } from 'react-router-dom';
3 | import { classNames } from 'shared/lib/classNames/classNames';
4 | import cls from './AppLink.module.scss';
5 |
6 | export enum AppLinkTheme {
7 | PRIMARY = 'primary',
8 | SECONDARY = 'secondary',
9 | RED = 'red'
10 | }
11 |
12 | interface AppLinkProps extends LinkProps {
13 | className?: string;
14 | theme?: AppLinkTheme;
15 | children?: ReactNode;
16 | }
17 |
18 | export const AppLink = memo((props: AppLinkProps) => {
19 | const {
20 | to,
21 | className = '',
22 | children,
23 | theme = AppLinkTheme.PRIMARY,
24 | ...otherProps
25 | } = props;
26 | return (
27 |
32 | {children}
33 |
34 | );
35 | });
36 |
--------------------------------------------------------------------------------
/src/entities/Counter/ui/Counter.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'shared/ui/Button/Button';
2 | import { counterActions } from '../model/slice/counterSlice';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { getCounterValue } from '../model/selectors/getCounterValue/getCounterValue';
5 |
6 | export const Counter = () => {
7 | const dispatch = useDispatch();
8 | const counterValue = useSelector(getCounterValue);
9 | const increment = () => {
10 | dispatch(counterActions.increment());
11 | };
12 |
13 | const decrement = () => {
14 | dispatch(counterActions.decrement());
15 | };
16 | return (
17 |
18 |
{counterValue}
19 |
22 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/shared/assets/icons/main-20-20.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/entities/User/model/slice/userSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { USER_LOCALSTORAGE_KEY } from 'shared/const/localstorage';
3 | import { User, UserSchema } from '../types/user';
4 |
5 | const initialState: UserSchema = {};
6 |
7 | export const userSlice = createSlice({
8 | name: 'user',
9 | initialState,
10 | reducers: {
11 | setAuthData: (state, action: PayloadAction) => {
12 | state.authData = action.payload;
13 | },
14 | initAuthData: (state) => {
15 | const user = localStorage.getItem(USER_LOCALSTORAGE_KEY);
16 | if (user) {
17 | state.authData = JSON.parse(user);
18 | }
19 | },
20 | logout: (state) => {
21 | state.authData = undefined;
22 | localStorage.removeItem(USER_LOCALSTORAGE_KEY);
23 | }
24 | }
25 | });
26 |
27 | export const { actions: userActions } = userSlice;
28 | export const { reducer: userReducer } = userSlice;
29 |
--------------------------------------------------------------------------------
/src/pages/ProfilePage/ui/ProfilePage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import ProfilePage from './ProfilePage';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 | import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator';
8 |
9 | export default {
10 | title: 'pages/ProfilePage',
11 | component: ProfilePage,
12 | argTypes: {
13 | backgroundColor: { control: 'color' }
14 | }
15 | } as ComponentMeta;
16 |
17 | const Template: ComponentStory = (args) => ;
18 |
19 | export const Normal = Template.bind({});
20 | Normal.args = {};
21 | Normal.decorators = [StoreDecorator({})];
22 |
23 | export const Dark = Template.bind({});
24 | Dark.args = {};
25 |
26 | Dark.decorators = [ThemeDecorator(Theme.DARK), StoreDecorator({})];
27 |
--------------------------------------------------------------------------------
/src/shared/config/storybook/StoreDecorator/StoreDecorator.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial, ReducersMapObject } from '@reduxjs/toolkit';
2 | import { Story } from '@storybook/react';
3 | import { StateSchema, StoreProvider } from 'app/providers/StoreProvider';
4 | import { profileReducer } from 'entities/Profile';
5 | import { loginReducer } from 'features/AuthByUsername/model/slice/loginSlice';
6 |
7 | const defaultAsyncReducers: DeepPartial> = {
8 | loginForm: loginReducer,
9 | profile: profileReducer
10 | };
11 |
12 | // test
13 | export const StoreDecorator =
14 | (
15 | state: DeepPartial,
16 | asyncReducers?: DeepPartial>
17 | ) =>
18 | (StoryComponent: Story) => {
19 | return (
20 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/config/build/buildWebpackConfig.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions } from './types/config';
2 | import webpack from 'webpack';
3 | import webpackDevServer from 'webpack-dev-server';
4 | import { buildPlugins } from './buildPlugins';
5 | import { buildLoaders } from './buildLoaders';
6 | import { buildResolvers } from './buildResolvers';
7 | import { buildDevServer } from './buildDevServer';
8 |
9 | export function buildWebpackConfig(
10 | options: BuildOptions
11 | ): webpack.Configuration {
12 | const { paths, mode, isDev } = options;
13 | return {
14 | mode,
15 | entry: paths.entry,
16 | output: {
17 | filename: '[name].[contenthash].js',
18 | path: paths.build,
19 | clean: true
20 | },
21 | plugins: buildPlugins(options),
22 | module: {
23 | rules: buildLoaders(options)
24 | },
25 | resolve: buildResolvers(options),
26 | devtool: isDev ? 'inline-source-map' : undefined,
27 | devServer: isDev ? buildDevServer(options) : undefined
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/shared/lib/tests/componentRender/componentRender.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { ReactNode } from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import { I18nextProvider } from 'react-i18next';
5 | import i18nForTests from 'shared/config/i18n/i18nForTests';
6 | import { StateSchema, StoreProvider } from 'app/providers/StoreProvider';
7 | import { DeepPartial } from '@reduxjs/toolkit';
8 |
9 | export interface componentRenderOptions {
10 | route?: string;
11 | initialState?: DeepPartial;
12 | }
13 |
14 | export function componentRender(
15 | component: ReactNode,
16 | options: componentRenderOptions = {}
17 | ) {
18 | const { route = '/', initialState } = options;
19 | return render(
20 |
21 |
22 | {component}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/services/loginByUsername/loginByUsername.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { createAsyncThunk } from '@reduxjs/toolkit';
3 | import { User, userActions } from 'entities/User';
4 | import { USER_LOCALSTORAGE_KEY } from 'shared/const/localstorage';
5 |
6 | interface LoginByUsername {
7 | username: string;
8 | password: string;
9 | }
10 |
11 | export const loginByUsername = createAsyncThunk<
12 | User,
13 | LoginByUsername,
14 | { rejectValue: string }
15 | >('login/loginByUsername', async (authData, thunkAPI) => {
16 | try {
17 | const response = await axios.post('http://localhost:8000/login', authData);
18 | if (!response.data) {
19 | throw new Error();
20 | }
21 | localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(response.data));
22 | thunkAPI.dispatch(userActions.setAuthData(response.data));
23 | return response.data;
24 | } catch (error) {
25 | console.log(error);
26 | return thunkAPI.rejectWithValue('error');
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | //hightlights where you're not added type
5 | "noImplicitAny": true,
6 | "module": "ESNext",
7 | //in which version will be compiled
8 | "target": "es5",
9 | "jsx": "react-jsx",
10 | //compiler can use not only ts files but also js files
11 | "allowJs": true,
12 | "moduleResolution": "node",
13 | //for an absolute import
14 | "baseUrl": ".",
15 | "paths": {
16 | "*": ["./src/*"]
17 | },
18 | // you can use common js packages like a normal one with import (require() module.export) = common js
19 | "esModuleInterop": true,
20 | //if library doesn't support default import this attribute help us to use import React from 'react' instead import * as React from 'react'
21 | "allowSyntheticDefaultImports": true,
22 | "strictNullChecks": true
23 | },
24 | "ts-node": {
25 | "compilerOptions": {
26 | "module": "CommonJS"
27 | }
28 | },
29 | "include": ["./src/**/*.ts", "./src/**/*.tsx"]
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/providers/StoreProvider/config/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, ReducersMapObject } from '@reduxjs/toolkit';
2 | import { counterReducer } from 'entities/Counter';
3 | import { userReducer } from 'entities/User';
4 | import { createReducerManager } from './reduceManager';
5 | import { StateSchema } from './StateSchema';
6 |
7 | export function createReduxStore(
8 | initialState?: StateSchema,
9 | asyncReducers?: ReducersMapObject
10 | ) {
11 | const rootReducers: ReducersMapObject = {
12 | ...asyncReducers,
13 | counter: counterReducer,
14 | user: userReducer
15 | };
16 |
17 | const reducerManager = createReducerManager(rootReducers);
18 |
19 | const store = configureStore({
20 | reducer: reducerManager.reduce,
21 | devTools: __IS_DEV__,
22 | preloadedState: initialState
23 | });
24 |
25 | // @ts-expect-error
26 | store.reducerManager = reducerManager;
27 |
28 | return store;
29 | }
30 |
31 | export type AppDispatch = ReturnType['dispatch'];
32 |
--------------------------------------------------------------------------------
/src/entities/Counter/ui/Counter.test.tsx:
--------------------------------------------------------------------------------
1 | import { screen } from '@testing-library/react';
2 | import { componentRender } from 'shared/lib/tests/componentRender/componentRender';
3 | import { userEvent } from '@storybook/testing-library';
4 | import { Counter } from './Counter';
5 |
6 | describe('Counter', () => {
7 | test('test render', () => {
8 | componentRender(, { initialState: { counter: { value: 10 } } });
9 | expect(screen.getByTestId('value-title')).toHaveTextContent('10');
10 | });
11 |
12 | test('increment', () => {
13 | componentRender(, { initialState: { counter: { value: 10 } } });
14 | userEvent.click(screen.getByTestId('increment-btn'));
15 | expect(screen.getByTestId('value-title')).toHaveTextContent('11');
16 | });
17 |
18 | test('decrement', () => {
19 | componentRender(, { initialState: { counter: { value: 10 } } });
20 | userEvent.click(screen.getByTestId('decrement-btn'));
21 | expect(screen.getByTestId('value-title')).toHaveTextContent('9');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/providers/StoreProvider/config/StateSchema.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CombinedState,
3 | EnhancedStore,
4 | Reducer,
5 | ReducersMapObject
6 | } from '@reduxjs/toolkit';
7 | import { CounterSchema } from 'entities/Counter';
8 | import { ProfileSchema } from 'entities/Profile';
9 | import { UserSchema } from 'entities/User';
10 | import { LoginSchema } from 'features/AuthByUsername';
11 |
12 | export interface StateSchema {
13 | counter: CounterSchema;
14 | user: UserSchema;
15 |
16 | // Async reducers
17 | loginForm?: LoginSchema;
18 | profile?: ProfileSchema;
19 | }
20 |
21 | export type StateSchemaKey = keyof StateSchema;
22 |
23 | export interface ReducerManager {
24 | getReducerMap: () => ReducersMapObject;
25 | reduce: (state: StateSchema, action: any) => CombinedState;
26 | add: (key: StateSchemaKey, reducer: Reducer) => void;
27 | remove: (key: StateSchemaKey) => void;
28 | }
29 |
30 | export interface ReduxStoreWithManager extends EnhancedStore {
31 | reducerManager: ReducerManager;
32 | }
33 |
--------------------------------------------------------------------------------
/scripts/generate-visual-json-report.js:
--------------------------------------------------------------------------------
1 | const { promisify } = require('util');
2 | const { readdir, writeFile } = require('fs');
3 | const { join: joinPath, relative } = require('path');
4 |
5 | const asyncReaddir = promisify(readdir);
6 | const writeFileAsync = promisify(writeFile);
7 |
8 | const lokiDir = joinPath(__dirname, '..', '.loki');
9 | const actualDir = joinPath(lokiDir, 'current');
10 | const expectedDir = joinPath(lokiDir, 'reference');
11 | const diffDir = joinPath(lokiDir, 'difference');
12 |
13 | (async function main() {
14 | const diffs = await asyncReaddir(diffDir);
15 |
16 | await writeFileAsync(
17 | joinPath(lokiDir, 'report.json'),
18 | JSON.stringify({
19 | newItems: [],
20 | deletedItems: [],
21 | passedItems: [],
22 | failedItems: diffs,
23 | expectedItems: diffs,
24 | actualItems: diffs,
25 | diffItems: diffs,
26 | actualDir: relative(lokiDir, actualDir),
27 | expectedDir: relative(lokiDir, expectedDir),
28 | diffDir: relative(lokiDir, diffDir)
29 | })
30 | );
31 | })();
32 |
--------------------------------------------------------------------------------
/src/shared/config/routeConfig/routeConfig.tsx:
--------------------------------------------------------------------------------
1 | import { AboutPage } from 'pages/AboutPage';
2 | import { MainPage } from 'pages/MainPage';
3 | import { NotFoundPage } from 'pages/NotFoundPage';
4 | import { ProfilePage } from 'pages/ProfilePage';
5 | import { RouteProps } from 'react-router-dom';
6 |
7 | export enum AppRoutes {
8 | MAIN = 'main',
9 | ABOUT = 'about',
10 | PROFILE = 'profile',
11 | // last
12 | NOT_FOUND = 'not_found'
13 | }
14 |
15 | export const RoutePath: Record = {
16 | [AppRoutes.MAIN]: '/',
17 | [AppRoutes.ABOUT]: '/about',
18 | [AppRoutes.PROFILE]: '/profile',
19 | [AppRoutes.NOT_FOUND]: '*'
20 | };
21 |
22 | export const routeConfig: Record = {
23 | [AppRoutes.MAIN]: {
24 | path: RoutePath.main,
25 | element:
26 | },
27 | [AppRoutes.ABOUT]: {
28 | path: RoutePath.about,
29 | element:
30 | },
31 | [AppRoutes.PROFILE]: {
32 | path: RoutePath.profile,
33 | element:
34 | },
35 | [AppRoutes.NOT_FOUND]: {
36 | path: RoutePath.not_found,
37 | element:
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: linting, testing, building
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 | jobs:
8 | pipeline:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node-version: [17.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Staring Node.js ${{ matrix.node-version}}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - name: install modules
21 | run: npm install
22 | - name: build production project
23 | run: npm run build:prod
24 | if: always()
25 | - name: linting typescript
26 | run: npm run lint:ts
27 | if: always()
28 | - name: linting css
29 | run: npm run lint:scss
30 | if: always()
31 | - name: unit testing
32 | run: npm run test:unit
33 | if: always()
34 | - name: build storybook
35 | run: npm run storybook:build
36 | if: always()
37 | - name: screenshot testing
38 | run: npm run test:ui:ci
39 | if: always()
40 |
--------------------------------------------------------------------------------
/config/storybook/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { RuleSetRule, Configuration, DefinePlugin } from 'webpack';
3 | import { buildCssLoader } from '../build/loaders/buildCssLoader';
4 | import { BuildPaths } from '../build/types/config';
5 |
6 | export default ({ config }: { config: Configuration }) => {
7 | const paths: BuildPaths = {
8 | build: '',
9 | html: '',
10 | entry: '',
11 | src: path.resolve(__dirname, '..', '..', 'src')
12 | };
13 | config.resolve?.modules?.unshift(paths.src);
14 | config.resolve?.extensions?.push('.ts', '.tsx');
15 |
16 | if (config.module?.rules) {
17 | config.module.rules = config.module?.rules?.map((rule: any) => {
18 | if (/svg/.test(rule.test as string)) {
19 | return { ...rule, exclude: /\.svg$/i };
20 | }
21 | return rule;
22 | });
23 | }
24 |
25 | config.module?.rules?.push({
26 | test: /\.svg$/,
27 | use: ['@svgr/webpack']
28 | });
29 | config.module?.rules?.push(buildCssLoader(true));
30 |
31 | config.plugins?.push(
32 | new DefinePlugin({
33 | __IS_DEV__: true
34 | })
35 | );
36 |
37 | return config;
38 | };
39 |
--------------------------------------------------------------------------------
/config/build/buildPlugins.ts:
--------------------------------------------------------------------------------
1 | import HTMLWebpackPlugin from 'html-webpack-plugin';
2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
3 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
5 | import webpack from 'webpack';
6 | import { BuildOptions } from './types/config';
7 |
8 | export function buildPlugins({
9 | paths,
10 | isDev
11 | }: BuildOptions): webpack.WebpackPluginInstance[] {
12 | const plugins = [
13 | new HTMLWebpackPlugin({
14 | template: paths.html
15 | }),
16 | new webpack.ProgressPlugin(),
17 | new MiniCssExtractPlugin({
18 | filename: 'css/[name].[contenthash:8].css',
19 | chunkFilename: 'css/[name].[contenthash:8].css'
20 | }),
21 | new webpack.DefinePlugin({
22 | __IS_DEV__: JSON.stringify(isDev)
23 | })
24 | ];
25 |
26 | if (isDev) {
27 | plugins.push(new webpack.HotModuleReplacementPlugin());
28 | plugins.push(
29 | new BundleAnalyzerPlugin({
30 | openAnalyzer: false
31 | })
32 | );
33 | plugins.push(new ReactRefreshWebpackPlugin());
34 | }
35 |
36 | return plugins;
37 | }
38 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentStory, ComponentMeta } from '@storybook/react';
2 | import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator';
3 | import LoginForm from './LoginForm';
4 |
5 | export default {
6 | title: 'features/LoginForm',
7 | component: LoginForm,
8 | argTypes: {
9 | backgroundColor: { control: 'color' }
10 | }
11 | } as ComponentMeta;
12 |
13 | const Template: ComponentStory = (args) => ;
14 |
15 | export const Primary = Template.bind({});
16 | Primary.args = {};
17 | Primary.decorators = [
18 | StoreDecorator({ loginForm: { username: '123', password: 'sdfsdf', isLoading: false } })
19 | ];
20 |
21 | export const withError = Template.bind({});
22 | withError.args = {};
23 | withError.decorators = [
24 | StoreDecorator({
25 | loginForm: { username: '123', password: 'sdfsdf', error: 'ERROR', isLoading: false }
26 | })
27 | ];
28 |
29 | export const Loading = Template.bind({});
30 | Loading.args = {};
31 | Loading.decorators = [
32 | StoreDecorator({ loginForm: { username: '123', password: 'sdfsdf', isLoading: true } })
33 | ];
34 |
--------------------------------------------------------------------------------
/config/build/buildLoaders.ts:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import { BuildOptions } from './types/config';
3 | import { buildCssLoader } from './loaders/buildCssLoader';
4 |
5 | export function buildLoaders({ isDev }: BuildOptions): webpack.RuleSetRule[] {
6 | const svgLoader = {
7 | test: /\.svg$/,
8 | use: ['@svgr/webpack']
9 | };
10 |
11 | const babelLoader = {
12 | test: /\.(js|jsx|tsx)$/,
13 | exclude: /node_modules/,
14 | use: {
15 | loader: 'babel-loader',
16 | options: {
17 | presets: ['@babel/preset-env'],
18 | plugins: [['i18next-extract', { locales: ['ru', 'en'], keyAsDefaultValue: true }]]
19 | }
20 | }
21 | };
22 |
23 | const cssLoader = buildCssLoader(isDev);
24 |
25 | // If we don't use typescript - we have to add babel-loader
26 | const typescriptloader = {
27 | test: /\.tsx?$/,
28 | use: 'ts-loader',
29 | exclude: /node_modules/
30 | };
31 |
32 | const fileLoader = {
33 | test: /\.(png|jpe?g|gif|woff2|woff)$/i,
34 | loader: 'file-loader',
35 | options: {
36 | name: '[path][name].[ext]'
37 | }
38 | };
39 |
40 | return [fileLoader, svgLoader, babelLoader, typescriptloader, cssLoader];
41 | }
42 |
--------------------------------------------------------------------------------
/src/shared/lib/classNames/classNames.test.ts:
--------------------------------------------------------------------------------
1 | import { classNames } from 'shared/lib/classNames/classNames';
2 |
3 | describe('classNames', () => {
4 | test('with first param', () => {
5 | expect(classNames('someClass', {}, [])).toBe('someClass');
6 | });
7 |
8 | test('with additional class', () => {
9 | const expected = 'someClass class1 class2';
10 | expect(classNames('someClass', {}, ['class1', 'class2'])).toBe(expected);
11 | });
12 |
13 | test('with additional class', () => {
14 | const expected = 'someClass class1 class2 hovered scrollable';
15 | expect(
16 | classNames('someClass', { hovered: true, scrollable: true }, ['class1', 'class2'])
17 | ).toBe(expected);
18 | });
19 |
20 | test('with additional class', () => {
21 | const expected = 'someClass class1 class2 hovered';
22 | expect(
23 | classNames('someClass', { hovered: true, scrollable: false }, ['class1', 'class2'])
24 | ).toBe(expected);
25 | });
26 |
27 | test('with additional class', () => {
28 | const expected = 'someClass class1 class2 hovered';
29 | expect(
30 | classNames('someClass', { hovered: true, scrollable: '' }, ['class1', 'class2'])
31 | ).toBe(expected);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/widgets/Navbar/ui/Navbar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
5 | import { Theme } from 'app/providers/ThemeProvider';
6 | import { Navbar } from './Navbar';
7 | import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator';
8 |
9 | export default {
10 | title: 'widget/Navbar',
11 | component: Navbar,
12 | argTypes: {
13 | backgroundColor: { control: 'color' }
14 | }
15 | } as ComponentMeta;
16 |
17 | const Template: ComponentStory = (args) => ;
18 |
19 | export const Light = Template.bind({});
20 | Light.args = {};
21 | Light.decorators = [StoreDecorator({ user: { authData: undefined } })];
22 |
23 | export const AurhenticatedUser = Template.bind({});
24 | AurhenticatedUser.args = {};
25 | AurhenticatedUser.decorators = [
26 | StoreDecorator({ user: { authData: { id: '2', username: '3434' } } })
27 | ];
28 |
29 | export const Dark = Template.bind({});
30 | Dark.args = {};
31 | Dark.decorators = [
32 | ThemeDecorator(Theme.DARK),
33 | StoreDecorator({ user: { authData: undefined } })
34 | ];
35 |
--------------------------------------------------------------------------------
/src/shared/ui/Loader/Loader.scss:
--------------------------------------------------------------------------------
1 | .lds-ellipsis {
2 | display: inline-block;
3 | position: relative;
4 | width: 80px;
5 | height: 80px;
6 | }
7 |
8 | .lds-ellipsis div {
9 | position: absolute;
10 | top: 33px;
11 | width: 13px;
12 | height: 13px;
13 | border-radius: 50%;
14 | background: var(--inverted-bg-color);
15 | animation-timing-function: cubic-bezier(0, 1, 1, 0);
16 | }
17 |
18 | .lds-ellipsis div:nth-child(1) {
19 | left: 8px;
20 | animation: lds-ellipsis1 0.6s infinite;
21 | }
22 |
23 | .lds-ellipsis div:nth-child(2) {
24 | left: 8px;
25 | animation: lds-ellipsis2 0.6s infinite;
26 | }
27 |
28 | .lds-ellipsis div:nth-child(3) {
29 | left: 32px;
30 | animation: lds-ellipsis2 0.6s infinite;
31 | }
32 |
33 | .lds-ellipsis div:nth-child(4) {
34 | left: 56px;
35 | animation: lds-ellipsis3 0.6s infinite;
36 | }
37 |
38 | @keyframes lds-ellipsis1 {
39 | 0% {
40 | transform: scale(0);
41 | }
42 |
43 | 100% {
44 | transform: scale(1);
45 | }
46 | }
47 |
48 | @keyframes lds-ellipsis3 {
49 | 0% {
50 | transform: scale(1);
51 | }
52 |
53 | 100% {
54 | transform: scale(0);
55 | }
56 | }
57 |
58 | @keyframes lds-ellipsis2 {
59 | 0% {
60 | transform: translate(0, 0);
61 | }
62 |
63 | 100% {
64 | transform: translate(24px, 0);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/slice/loginSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { loginByUsername } from '../services/loginByUsername/loginByUsername';
3 | import { LoginSchema } from '../types/loginSchema';
4 |
5 | const initialState: LoginSchema = {
6 | isLoading: false,
7 | username: '',
8 | password: ''
9 | };
10 |
11 | export const loginSlice = createSlice({
12 | name: 'login',
13 | initialState,
14 | reducers: {
15 | setUsername: (state, action: PayloadAction) => {
16 | state.username = action.payload;
17 | },
18 | setPassword: (state, action: PayloadAction) => {
19 | state.password = action.payload;
20 | }
21 | },
22 | extraReducers: (builder) => {
23 | builder
24 | .addCase(loginByUsername.pending, (state, action) => {
25 | state.error = undefined;
26 | state.isLoading = true;
27 | })
28 | .addCase(loginByUsername.fulfilled, (state, action) => {
29 | state.isLoading = false;
30 | })
31 | .addCase(loginByUsername.rejected, (state, action) => {
32 | state.isLoading = false;
33 | state.error = action.payload;
34 | });
35 | }
36 | });
37 |
38 | export const { actions: loginActions } = loginSlice;
39 | export const { reducer: loginReducer } = loginSlice;
40 |
--------------------------------------------------------------------------------
/src/app/providers/ErrorBoundary/ui/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ErrorInfo, ReactNode, Suspense } from 'react';
2 | import { PageError } from 'widgets/PageError/ui/PageError';
3 |
4 | interface ErrorBoundaryProps {
5 | children: ReactNode;
6 | }
7 |
8 | interface ErrorBoundaryState {
9 | hasError: boolean;
10 | }
11 |
12 | class ErrorBoundary extends React.Component<
13 | ErrorBoundaryProps,
14 | ErrorBoundaryState
15 | > {
16 | constructor(props: ErrorBoundaryProps) {
17 | super(props);
18 | this.state = { hasError: false };
19 | }
20 |
21 | static getDerivedStateFromError(error: Error) {
22 | if (error) {
23 | console.log(error);
24 | }
25 | // Update state so the next render will show the fallback UI.
26 | return { hasError: true };
27 | }
28 |
29 | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
30 | // You can also log the error to an error reporting service
31 | console.log(error, errorInfo);
32 | }
33 |
34 | render() {
35 | const { hasError } = this.state;
36 | const { children } = this.props;
37 |
38 | if (hasError) {
39 | // You can render any custom fallback UI
40 | return (
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | return children;
48 | }
49 | }
50 |
51 | export default ErrorBoundary;
52 |
--------------------------------------------------------------------------------
/src/app/providers/StoreProvider/config/reduceManager.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, Reducer, ReducersMapObject } from '@reduxjs/toolkit';
2 | import { ReducerManager, StateSchema, StateSchemaKey } from './StateSchema';
3 |
4 | export function createReducerManager(
5 | initialReducers: ReducersMapObject
6 | ): ReducerManager {
7 | const reducers = { ...initialReducers };
8 |
9 | let combinedReducer = combineReducers(reducers);
10 | let keysToRemove: StateSchemaKey[] = [];
11 |
12 | return {
13 | getReducerMap: () => reducers,
14 | reduce: (state: StateSchema, action: any) => {
15 | if (keysToRemove.length > 0) {
16 | state = { ...state };
17 | keysToRemove.forEach((key) => {
18 | delete state[key];
19 | });
20 | keysToRemove = [];
21 | }
22 | return combinedReducer(state, action);
23 | },
24 |
25 | add: (key: StateSchemaKey, reducer: Reducer) => {
26 | if (!key || reducers[key]) {
27 | return;
28 | }
29 | reducers[key] = reducer;
30 | combinedReducer = combineReducers(reducers);
31 | },
32 |
33 | remove: (key: StateSchemaKey) => {
34 | if (!key || !reducers[key]) {
35 | return;
36 | }
37 | delete reducers[key];
38 | keysToRemove.push(key);
39 | combinedReducer = combineReducers(reducers);
40 | }
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/shared/ui/Button/Button.module.scss:
--------------------------------------------------------------------------------
1 | .Button {
2 | cursor: pointer;
3 | color: var(--secondary-color);
4 | padding: 6px 15px;
5 | }
6 |
7 | .clear,
8 | .clearInverted {
9 | padding: 0;
10 | margin: 0;
11 | border: none;
12 | background: none;
13 | outline: none;
14 | }
15 |
16 | .clearInverted {
17 | color: var(--inverted-primary-color);
18 | }
19 |
20 | .outline {
21 | border: 1px solid var(--primary-color);
22 | color: var(--primary-color);
23 | background: none;
24 | }
25 |
26 | .background {
27 | background: var(--bg-color);
28 | color: var(--primary-color);
29 | border: none;
30 | }
31 |
32 | .backgroundInverted {
33 | background: var(--inverted-bg-color);
34 | color: var(--inverted-primary-color);
35 | border: none;
36 | }
37 |
38 | .square {
39 | padding: 0;
40 | }
41 |
42 | .square.size_m {
43 | height: var(--font-line-m);
44 | width: var(--font-line-m);
45 | font: var(--font-m);
46 | }
47 |
48 | .square.size_l {
49 | height: var(--font-line-l);
50 | width: var(--font-line-l);
51 | font: var(--font-l);
52 | }
53 |
54 | .square.size_xl {
55 | height: var(--font-line-xl);
56 | width: var(--font-line-xl);
57 | font: var(--font-xl);
58 | }
59 |
60 | .size_m {
61 | font: var(--font-m);
62 | }
63 |
64 | .size_l {
65 | font: var(--font-l);
66 | }
67 |
68 | .size_xl {
69 | font: var(--font-xl);
70 | }
71 |
72 | .disabled {
73 | opacity: 0.5;
74 | }
75 |
--------------------------------------------------------------------------------
/src/shared/ui/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import cls from './Button.module.scss';
4 |
5 | export enum ButtonTheme {
6 | CLEAR = 'clear',
7 | CLEAR_INVERTED = 'clearInverted',
8 | OUTLINE = 'outline',
9 | BACKGROUND = 'background',
10 | BACKGROUND_INVERTED = 'backgroundInverted'
11 | }
12 |
13 | export enum ButtonSize {
14 | M = 'size_m',
15 | L = 'size_l',
16 | XL = 'size_xl'
17 | }
18 |
19 | interface ButtonProps extends ButtonHTMLAttributes {
20 | className?: string;
21 | theme?: ButtonTheme;
22 | square?: boolean;
23 | size?: ButtonSize;
24 | disabled?: boolean;
25 | children?: ReactNode;
26 | }
27 |
28 | export const Button = memo((props: ButtonProps) => {
29 | const {
30 | className = '',
31 | children,
32 | theme = '',
33 | square = false,
34 | size = ButtonSize.M,
35 | disabled,
36 | ...otherProps
37 | } = props;
38 | const mods: Record = {
39 | [cls[theme]]: true,
40 | [cls.square]: square,
41 | [cls[size]]: true,
42 | [cls.disabled]: !!disabled
43 | };
44 | return (
45 |
52 | );
53 | });
54 |
--------------------------------------------------------------------------------
/src/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader.tsx:
--------------------------------------------------------------------------------
1 | import { Reducer } from '@reduxjs/toolkit';
2 | import { ReduxStoreWithManager } from 'app/providers/StoreProvider';
3 | import { StateSchemaKey } from 'app/providers/StoreProvider/config/StateSchema';
4 | import { FC, useEffect } from 'react';
5 | import { useDispatch, useStore } from 'react-redux';
6 |
7 | export type ReducersList = {
8 | [name in StateSchemaKey]?: Reducer;
9 | };
10 |
11 | type ReducersListEntry = [StateSchemaKey, Reducer];
12 |
13 | interface DynamicModuleLoaderProps {
14 | reducers: ReducersList;
15 | removeAfterUnmount?: boolean;
16 | }
17 |
18 | export const DynamicModuleLoader: FC = (props) => {
19 | const { children, reducers, removeAfterUnmount } = props;
20 | const dispatch = useDispatch();
21 | const store = useStore() as ReduxStoreWithManager;
22 |
23 | useEffect(() => {
24 | Object.entries(reducers).forEach(([name, reducer]: ReducersListEntry) => {
25 | store.reducerManager.add(name, reducer);
26 | dispatch({ type: `@INIT ${name} reducer` });
27 | });
28 |
29 | return () => {
30 | if (removeAfterUnmount) {
31 | Object.entries(reducers).forEach(([name, reducer]: ReducersListEntry) => {
32 | store.reducerManager.remove(name);
33 | dispatch({ type: `@DESTROY ${name} reducer` });
34 | });
35 | }
36 | };
37 | // eslint-disable-next-line
38 | }, []);
39 |
40 | return <>{children}>;
41 | };
42 |
--------------------------------------------------------------------------------
/json-server/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const jsonServer = require('json-server');
3 | const path = require('path');
4 |
5 | const server = jsonServer.create();
6 |
7 | const router = jsonServer.router(path.resolve(__dirname, 'db.json'));
8 |
9 | server.use(jsonServer.defaults({}));
10 | server.use(jsonServer.bodyParser);
11 |
12 | server.use(async (req, res, next) => {
13 | await new Promise((resolve) => {
14 | setTimeout(resolve, 800);
15 | });
16 | next();
17 | });
18 |
19 | // login endpoint
20 | server.post('/login', (req, res) => {
21 | try {
22 | const { username, password } = req.body;
23 | const db = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'db.json'), 'UTF-8'));
24 | const { users = [] } = db;
25 |
26 | const userFromBd = users.find(
27 | (user) => user.username === username && user.password === password
28 | );
29 |
30 | if (userFromBd) {
31 | return res.json(userFromBd);
32 | }
33 |
34 | return res.status(403).json({ message: 'User not found' });
35 | } catch (e) {
36 | console.log(e);
37 | return res.status(500).json({ message: e.message });
38 | }
39 | });
40 |
41 | // check if user is authorized
42 | server.use((req, res, next) => {
43 | if (!req.headers.authorization) {
44 | return res.status(403).json({ message: 'AUTH ERROR' });
45 | }
46 |
47 | next();
48 | });
49 |
50 | server.use(router);
51 |
52 | // run server
53 | server.listen(8000, () => {
54 | console.log('server is running on 8000 port');
55 | });
56 |
--------------------------------------------------------------------------------
/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from 'react';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import { Button, ButtonSize, ButtonTheme } from 'shared/ui/Button/Button';
4 | import { LangSwitcher } from 'widgets/LangSwitcher/LangSwitcher';
5 | import { ThemeSwitcher } from 'widgets/ThemeSwitcher';
6 | import cls from './Sidebar.module.scss';
7 | import { SidebarItemsList } from '../../model/items';
8 | import { SidebarItem } from '../SidebarItem/SidebarItem';
9 |
10 | interface SidebarProps {
11 | className?: string;
12 | }
13 |
14 | export const Sidebar = memo(({ className = '' }: SidebarProps) => {
15 | const [collapsed, setCollapsed] = useState(false);
16 |
17 | const onToggle = () => {
18 | setCollapsed((prev) => !prev);
19 | };
20 |
21 | return (
22 |
26 |
36 |
37 | {SidebarItemsList.map((item) => (
38 |
39 | ))}
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/src/shared/assets/icons/about-20-20.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/shared/ui/Text/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentStory, ComponentMeta } from '@storybook/react';
2 | import { Theme } from 'app/providers/ThemeProvider';
3 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
4 | import { Text, TextTheme } from './Text';
5 |
6 | export default {
7 | title: 'shared/Text',
8 | component: Text,
9 | argTypes: {
10 | backgroundColor: { control: 'color' }
11 | }
12 | } as ComponentMeta;
13 |
14 | const Template: ComponentStory = (args) => ;
15 |
16 | export const Error = Template.bind({});
17 | Error.args = {
18 | title: 'Title fsdfdf',
19 | text: 'Description fsdfgdfg',
20 | theme: TextTheme.ERROR
21 | };
22 |
23 | export const Primary = Template.bind({});
24 | Primary.args = {
25 | title: 'Title fsdfdf',
26 | text: 'Description fsdfgdfg'
27 | };
28 | export const onlyTitle = Template.bind({});
29 | onlyTitle.args = {
30 | title: 'Title fsdfdf'
31 | };
32 | export const onlyText = Template.bind({});
33 | onlyText.args = {
34 | text: 'Description fsdfgdfg'
35 | };
36 |
37 | export const PrimaryDark = Template.bind({});
38 | PrimaryDark.args = {
39 | title: 'Title fsdfdf',
40 | text: 'Description fsdfgdfg'
41 | };
42 | PrimaryDark.decorators = [ThemeDecorator(Theme.DARK)];
43 | export const onlyTitleDark = Template.bind({});
44 | onlyTitleDark.args = {
45 | title: 'Title fsdfdf'
46 | };
47 | onlyTitleDark.decorators = [ThemeDecorator(Theme.DARK)];
48 | export const onlyTextDark = Template.bind({});
49 | onlyTextDark.args = {
50 | text: 'Description fsdfgdfg'
51 | };
52 | onlyTextDark.decorators = [ThemeDecorator(Theme.DARK)];
53 |
--------------------------------------------------------------------------------
/src/shared/assets/icons/theme-dark.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/shared/assets/icons/theme-light.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/shared/ui/AppLink/AppLink.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { AppLink, AppLinkTheme } from './AppLink';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'shared/AppLink',
10 | component: AppLink,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | },
14 | args: {
15 | to: '/'
16 | }
17 | } as ComponentMeta;
18 |
19 | const Template: ComponentStory = (args) => ;
20 |
21 | export const Primary = Template.bind({});
22 | Primary.args = {
23 | children: 'Text',
24 | theme: AppLinkTheme.PRIMARY
25 | };
26 |
27 | export const Secondary = Template.bind({});
28 | Secondary.args = {
29 | children: 'Text',
30 | theme: AppLinkTheme.SECONDARY
31 | };
32 |
33 | export const Red = Template.bind({});
34 | Red.args = {
35 | children: 'Text',
36 | theme: AppLinkTheme.RED
37 | };
38 |
39 | export const PrimaryDark = Template.bind({});
40 | PrimaryDark.args = {
41 | children: 'Text',
42 | theme: AppLinkTheme.PRIMARY
43 | };
44 |
45 | PrimaryDark.decorators = [ThemeDecorator(Theme.DARK)];
46 |
47 | export const SecondaryDark = Template.bind({});
48 | SecondaryDark.args = {
49 | children: 'Text',
50 | theme: AppLinkTheme.SECONDARY
51 | };
52 |
53 | SecondaryDark.decorators = [ThemeDecorator(Theme.DARK)];
54 |
55 | export const RedDark = Template.bind({});
56 | RedDark.args = {
57 | children: 'Text',
58 | theme: AppLinkTheme.RED
59 | };
60 |
61 | RedDark.decorators = [ThemeDecorator(Theme.DARK)];
62 |
--------------------------------------------------------------------------------
/src/widgets/Navbar/ui/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { getUserAuthData, userActions } from 'entities/User';
2 | import { LoginModal } from 'features/AuthByUsername';
3 | import { memo, useCallback, useState } from 'react';
4 | import { useTranslation } from 'react-i18next';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { classNames } from 'shared/lib/classNames/classNames';
7 | import { Button, ButtonTheme } from 'shared/ui/Button/Button';
8 | import cls from './Navbar.module.scss';
9 |
10 | interface NavbarProps {
11 | className?: string;
12 | }
13 |
14 | export const Navbar = memo(({ className = '' }: NavbarProps) => {
15 | const { t } = useTranslation();
16 |
17 | const [isAuthModal, setIsAuthModal] = useState(false);
18 | const authData = useSelector(getUserAuthData);
19 | const dispatch = useDispatch();
20 |
21 | const onClose = useCallback(() => {
22 | setIsAuthModal(false);
23 | }, []);
24 |
25 | const onShowModal = useCallback(() => {
26 | setIsAuthModal(true);
27 | }, []);
28 |
29 | const onLogout = useCallback(() => {
30 | dispatch(userActions.logout());
31 | }, [dispatch]);
32 |
33 | if (authData) {
34 | return (
35 |
36 |
37 |
40 |
41 |
42 | );
43 | }
44 |
45 | return (
46 |
47 |
48 |
51 |
52 | {isAuthModal &&
}
53 |
54 | );
55 | });
56 |
--------------------------------------------------------------------------------
/src/shared/ui/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { InputHTMLAttributes, memo, useEffect, useRef, useState } from 'react';
2 | import { classNames } from 'shared/lib/classNames/classNames';
3 | import cls from './Input.module.scss';
4 |
5 | type HTMLInputProps = Omit, 'value' | 'onChange'>;
6 |
7 | interface InputProps extends HTMLInputProps {
8 | className?: string;
9 | value?: string;
10 | onChange?: (value: string) => void;
11 | autofocus?: boolean;
12 | }
13 |
14 | export const Input = memo((props: InputProps) => {
15 | const {
16 | className = '',
17 | value,
18 | onChange,
19 | type = 'text',
20 | placeholder,
21 | autofocus,
22 | ...otherProps
23 | } = props;
24 |
25 | const ref = useRef(null);
26 | const [isFocused, setIsFocused] = useState(false);
27 | const [caretPosition, setCaretPosition] = useState(0);
28 |
29 | const onChangeHandler = (e: React.ChangeEvent) => {
30 | onChange?.(e.target.value);
31 | setCaretPosition(e.target.value.length);
32 | };
33 |
34 | const onBlur = () => {
35 | setIsFocused(false);
36 | };
37 |
38 | const onFocus = () => {
39 | setIsFocused(true);
40 | };
41 |
42 | const onSelect = (e: any) => {
43 | setCaretPosition(e?.target?.selectionStart || 0);
44 | };
45 |
46 | useEffect(() => {
47 | if (autofocus) {
48 | setIsFocused(true);
49 | ref.current?.focus();
50 | }
51 | }, [autofocus]);
52 |
53 | return (
54 |
55 | {placeholder &&
{`${placeholder}>`}
}
56 |
57 |
68 | {isFocused && (
69 |
70 | )}
71 |
72 |
73 | );
74 | });
75 |
--------------------------------------------------------------------------------
/src/shared/ui/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from 'app/providers/ThemeProvider';
2 | import React, { ReactNode, useCallback, useRef, useState, useEffect } from 'react';
3 | import { classNames } from 'shared/lib/classNames/classNames';
4 | import { Portal } from 'shared/ui/Portal/Portal';
5 | import cls from './Modal.module.scss';
6 |
7 | interface ModalProps {
8 | className?: string;
9 | children?: ReactNode;
10 | isOpen?: boolean;
11 | onClose?: () => void;
12 | lazy?: boolean;
13 | }
14 |
15 | const ANIMATION_DELAY = 300;
16 |
17 | export const Modal = ({
18 | className = '',
19 | children,
20 | isOpen = false,
21 | onClose,
22 | lazy
23 | }: ModalProps) => {
24 | const [isClosing, setIsClosing] = useState(false);
25 | const [isMounted, setIsMounted] = useState(false);
26 | const timeRef = useRef>();
27 | const mods: Record = {
28 | [cls.opened]: isOpen,
29 | [cls.isClosing]: isClosing
30 | };
31 | const { theme = '' } = useTheme();
32 |
33 | useEffect(() => {
34 | if (isOpen) {
35 | setIsMounted(true);
36 | }
37 | }, [isOpen]);
38 |
39 | const onContentClick = (e: React.MouseEvent) => {
40 | e.stopPropagation();
41 | };
42 |
43 | const closeHandler = useCallback(() => {
44 | if (onClose) {
45 | setIsClosing(true);
46 | timeRef.current = setTimeout(() => {
47 | onClose();
48 | setIsClosing(false);
49 | }, ANIMATION_DELAY);
50 | }
51 | }, [onClose]);
52 |
53 | const onKeyDown = useCallback(
54 | (e: KeyboardEvent) => {
55 | if (e.key === 'Escape') {
56 | closeHandler();
57 | }
58 | },
59 | [closeHandler]
60 | );
61 |
62 | useEffect(() => {
63 | if (isOpen) {
64 | window.addEventListener('keydown', onKeyDown);
65 | }
66 | return () => {
67 | clearTimeout(timeRef.current);
68 | window.removeEventListener('keydown', onKeyDown);
69 | };
70 | }, [isOpen, onKeyDown]);
71 |
72 | if (lazy && !isMounted) {
73 | return null;
74 | }
75 |
76 | return (
77 |
78 |
79 |
80 |
81 | {children}
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
--------------------------------------------------------------------------------
/src/shared/assets/icons/profile-20-20.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true
5 | },
6 | extends: [
7 | 'plugin:react/recommended',
8 | 'standard-with-typescript',
9 | 'plugin:i18next/recommended'
10 | ],
11 | overrides: [
12 | {
13 | files: ['**/src/**/*.{test,stories}.{ts,tsx}'],
14 | rules: {
15 | 'i18next/no-literal-string': 'off',
16 | 'max-len': 'off'
17 | }
18 | }
19 | ],
20 | parserOptions: {
21 | ecmaVersion: 'latest',
22 | sourceType: 'module',
23 | project: ['tsconfig.json']
24 | },
25 | ignorePatterns: [
26 | 'webpack.config.ts',
27 | 'buildDevServer.ts',
28 | 'buildLoaders.ts',
29 | 'buildPlugins.ts',
30 | 'buildResolvers.ts',
31 | 'buildWebpackConfig.ts',
32 | 'config.ts',
33 | 'jest.config.ts',
34 | 'jestEmptyComponent.tsx',
35 | 'setupTests.ts',
36 | 'buildCssLoader.ts'
37 | ],
38 | plugins: ['react', 'i18next', 'react-hooks'],
39 | rules: {
40 | 'react/react-in-jsx-scope': 'off',
41 | '@typescript-eslint/indent': 'off',
42 | 'react/jsx-indent': [2, 2],
43 | '@typescript-eslint/explicit-function-return-type': 'off',
44 | 'no-unused-vars': 'warn',
45 | '@typescript-eslint/no-unused-vars': 'warn',
46 | '@typescript-eslint/space-before-function-paren': 'off',
47 | '@typescript-eslint/member-delimiter-style': [
48 | 'error',
49 | {
50 | multiline: {
51 | delimiter: 'comma',
52 | requireLast: true
53 | },
54 | singleline: {
55 | delimiter: 'comma',
56 | requireLast: false
57 | },
58 | overrides: {
59 | interface: {
60 | multiline: {
61 | delimiter: 'semi',
62 | requireLast: true
63 | }
64 | }
65 | }
66 | }
67 | ],
68 | semi: ['error', 'always', { omitLastInOneLineBlock: true }],
69 | '@typescript-eslint/semi': 'off',
70 | '@typescript-eslint/no-floating-promises': 'off',
71 | '@typescript-eslint/strict-boolean-expressions': 'off',
72 | '@typescript-eslint/naming-convention': 'off',
73 | '@typescript-eslint/consistent-type-assertions': 'off',
74 | '@typescript-eslint/no-dynamic-delete': 'off',
75 | '@typescript-eslint/no-misused-promises': 'off',
76 | 'react/display-name': 'off',
77 | 'i18next/no-literal-string': [
78 | 'error',
79 | { markupOnly: true, ignoreAttribute: ['data-testid', 'to'] }
80 | ],
81 | 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
82 | 'react-hooks/exhaustive-deps': 'error' // Checks effect dependencies
83 | // 'react/require-default-props': 'off'
84 | }
85 | };
86 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/model/services/loginByUsername/loginByUsername.test.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { userActions } from 'entities/User';
3 | import { TestAsyncThunk } from 'shared/lib/tests/TestAsyncThunk/TestAsyncThunk';
4 | import { loginByUsername } from './loginByUsername';
5 |
6 | jest.mock('axios');
7 |
8 | const mockedAxios = jest.mocked(axios, true);
9 |
10 | describe('loginByUsername.test', () => {
11 | // let dispatch: Dispatch;
12 | // let getState: () => StateSchema;
13 |
14 | // beforeEach(() => {
15 | // dispatch = jest.fn();
16 | // getState = jest.fn();
17 | // });
18 |
19 | // test('success login', async () => {
20 | // const userValue = { username: '123', id: '1' };
21 | // mockedAxios.post.mockReturnValue(Promise.resolve({ data: userValue }));
22 | // const action = loginByUsername({ username: '123', password: '123' });
23 | // const result = await action(dispatch, getState, undefined);
24 |
25 | // expect(dispatch).toHaveBeenCalledWith(userActions.setAuthData(userValue));
26 | // expect(dispatch).toHaveBeenCalledTimes(3);
27 | // expect(mockedAxios.post).toHaveBeenCalled();
28 | // expect(result.meta.requestStatus).toBe('fulfilled');
29 | // expect(result.payload).toEqual(userValue);
30 | // });
31 |
32 | // test('error login', async () => {
33 | // mockedAxios.post.mockReturnValue(Promise.resolve({ status: 403 }));
34 | // const action = loginByUsername({ username: '123', password: '123' });
35 | // const result = await action(dispatch, getState, undefined);
36 |
37 | // expect(dispatch).toHaveBeenCalledTimes(2);
38 | // expect(mockedAxios.post).toHaveBeenCalled();
39 | // expect(result.meta.requestStatus).toBe('rejected');
40 | // expect(result.payload).toBe('error');
41 | // });
42 |
43 | test('success login', async () => {
44 | const userValue = { username: '123', id: '1' };
45 | mockedAxios.post.mockReturnValue(Promise.resolve({ data: userValue }));
46 | const thunk = new TestAsyncThunk(loginByUsername);
47 | const result = await thunk.callThunk({ username: '123', password: '123' });
48 |
49 | expect(thunk.dispatch).toHaveBeenCalledWith(userActions.setAuthData(userValue));
50 | expect(thunk.dispatch).toHaveBeenCalledTimes(3);
51 | expect(mockedAxios.post).toHaveBeenCalled();
52 | expect(result.meta.requestStatus).toBe('fulfilled');
53 | expect(result.payload).toEqual(userValue);
54 | });
55 |
56 | test('error login', async () => {
57 | mockedAxios.post.mockReturnValue(Promise.resolve({ status: 403 }));
58 | const thunk = new TestAsyncThunk(loginByUsername);
59 | const result = await thunk.callThunk({ username: '123', password: '123' });
60 |
61 | expect(thunk.dispatch).toHaveBeenCalledTimes(2);
62 | expect(mockedAxios.post).toHaveBeenCalled();
63 | expect(result.meta.requestStatus).toBe('rejected');
64 | expect(result.payload).toBe('error');
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/shared/ui/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 |
4 | import { Button, ButtonSize, ButtonTheme } from './Button';
5 | import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator';
6 | import { Theme } from 'app/providers/ThemeProvider';
7 |
8 | export default {
9 | title: 'shared/Button',
10 | component: Button,
11 | argTypes: {
12 | backgroundColor: { control: 'color' }
13 | }
14 | } as ComponentMeta;
15 |
16 | const Template: ComponentStory = (args) => ;
17 |
18 | export const Primary = Template.bind({});
19 | Primary.args = {
20 | children: 'Text'
21 | };
22 |
23 | export const Clear = Template.bind({});
24 | Clear.args = {
25 | children: 'Text',
26 | theme: ButtonTheme.CLEAR
27 | };
28 |
29 | export const ClearInverted = Template.bind({});
30 | ClearInverted.args = {
31 | children: 'Text',
32 | theme: ButtonTheme.CLEAR_INVERTED
33 | };
34 |
35 | export const Outlined = Template.bind({});
36 | Outlined.args = {
37 | children: 'Text',
38 | theme: ButtonTheme.OUTLINE
39 | };
40 |
41 | export const OutlinedSizeL = Template.bind({});
42 | OutlinedSizeL.args = {
43 | children: 'Text',
44 | theme: ButtonTheme.OUTLINE,
45 | size: ButtonSize.L
46 | };
47 |
48 | export const OutlinedSizeXL = Template.bind({});
49 | OutlinedSizeXL.args = {
50 | children: 'Text',
51 | theme: ButtonTheme.OUTLINE,
52 | size: ButtonSize.XL
53 | };
54 |
55 | export const OutlinedDark = Template.bind({});
56 | OutlinedDark.args = {
57 | children: 'Text',
58 | theme: ButtonTheme.OUTLINE
59 | };
60 |
61 | OutlinedDark.decorators = [ThemeDecorator(Theme.DARK)];
62 |
63 | export const BackgroundTheme = Template.bind({});
64 | BackgroundTheme.args = {
65 | children: 'Text',
66 | theme: ButtonTheme.BACKGROUND
67 | };
68 | export const BackgroundInverted = Template.bind({});
69 | BackgroundInverted.args = {
70 | children: 'Text',
71 | theme: ButtonTheme.BACKGROUND_INVERTED
72 | };
73 | export const Square = Template.bind({});
74 | Square.args = {
75 | children: '>',
76 | theme: ButtonTheme.BACKGROUND_INVERTED,
77 | square: true
78 | };
79 | export const SquareSizeM = Template.bind({});
80 | SquareSizeM.args = {
81 | children: '>',
82 | theme: ButtonTheme.BACKGROUND_INVERTED,
83 | square: true,
84 | size: ButtonSize.M
85 | };
86 | export const SquareSizeL = Template.bind({});
87 | SquareSizeL.args = {
88 | children: '>',
89 | theme: ButtonTheme.BACKGROUND_INVERTED,
90 | square: true,
91 | size: ButtonSize.L
92 | };
93 | export const SquareSizeXL = Template.bind({});
94 | SquareSizeXL.args = {
95 | children: '>',
96 | theme: ButtonTheme.BACKGROUND_INVERTED,
97 | square: true,
98 | size: ButtonSize.XL
99 | };
100 | export const Disabled = Template.bind({});
101 | Disabled.args = {
102 | children: '>',
103 | theme: ButtonTheme.OUTLINE,
104 | disabled: true
105 | };
106 |
--------------------------------------------------------------------------------
/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import { loginActions, loginReducer } from '../../model/slice/loginSlice';
2 | import { memo, useCallback } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 | import { useSelector } from 'react-redux';
5 | import { classNames } from 'shared/lib/classNames/classNames';
6 | import { Button, ButtonTheme } from 'shared/ui/Button/Button';
7 | import { Input } from 'shared/ui/Input/Input';
8 | import cls from './LoginForm.module.scss';
9 | import { loginByUsername } from '../../model/services/loginByUsername/loginByUsername';
10 | import { Text, TextTheme } from 'shared/ui/Text/Text';
11 | import { getLoginUsername } from '../../model/selectors/getLoginUsername/getLoginUsername';
12 | import { getLoginPassword } from '../../model/selectors/getLoginPassword/getLoginPassword';
13 | import { getLoginIsLoading } from '../../model/selectors/getLoginIsLoading/getLoginIsLoading';
14 | import { getLoginError } from '../../model/selectors/getLoginError/getLoginError';
15 | import {
16 | DynamicModuleLoader,
17 | ReducersList
18 | } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader';
19 | import { useAppDispatch } from 'shared/lib/hooks/useAppDispatch/useAppDispatch';
20 |
21 | export interface LoginFormProps {
22 | className?: string;
23 | onSuccess: () => void;
24 | }
25 |
26 | const initialReducers: ReducersList = {
27 | loginForm: loginReducer
28 | };
29 |
30 | const LoginForm = memo(({ className = '', onSuccess }: LoginFormProps) => {
31 | const { t } = useTranslation();
32 | const dispatch = useAppDispatch();
33 | const username = useSelector(getLoginUsername);
34 | const password = useSelector(getLoginPassword);
35 | const isLoading = useSelector(getLoginIsLoading);
36 | const error = useSelector(getLoginError);
37 |
38 | const onChangeUsername = useCallback(
39 | (value: string) => {
40 | dispatch(loginActions.setUsername(value));
41 | },
42 | [dispatch]
43 | );
44 |
45 | const onChangePassword = useCallback(
46 | (value: string) => {
47 | dispatch(loginActions.setPassword(value));
48 | },
49 | [dispatch]
50 | );
51 |
52 | const onLoginClick = useCallback(async () => {
53 | const result = await dispatch(loginByUsername({ username, password }));
54 | if (result.meta.requestStatus === 'fulfilled') {
55 | onSuccess();
56 | }
57 | }, [dispatch, onSuccess, password, username]);
58 |
59 | return (
60 |
61 |
62 |
63 | {error && (
64 |
65 | )}
66 |
74 |
81 |
89 |
90 |
91 | );
92 | });
93 |
94 | export default LoginForm;
95 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "production_react",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "start": "webpack serve --env port=3000",
9 | "start:dev:server": "node ./json-server/index.js",
10 | "build:prod": "webpack --env mode=production",
11 | "build:dev": "webpack --env mode=development",
12 | "lint:ts": "eslint \"**/*.{ts,tsx}\"",
13 | "lint:ts:fix": "eslint \"**/*.{ts,tsx}\" --fix",
14 | "lint:scss": "npx stylelint \"**/*.css\"",
15 | "lint:scss:fix": "npx stylelint \"**/*.css\" --fix",
16 | "test:unit": "jest --config ./config/jest/jest.config.ts",
17 | "test:ui": "npx loki test",
18 | "test:ui:ok": "npx loki approve",
19 | "test:ui:ci": "npx loki --requireReference --reactUri file:./storybook-static",
20 | "test:ui:report": "npm run test:ui:json && npm run test:ui:html",
21 | "test:ui:json": "node scripts/generate-visual-json-report.js",
22 | "test:ui:html": "reg-cli --from .loki/report.json --report .loki/report.html",
23 | "storybook": "start-storybook -p 6006 -c ./config/storybook",
24 | "storybook:build": "build-storybook -c ./config/storybook",
25 | "prepare": "husky install"
26 | },
27 | "keywords": [],
28 | "author": "",
29 | "license": "ISC",
30 | "devDependencies": {
31 | "@babel/core": "^7.17.5",
32 | "@babel/preset-env": "^7.16.11",
33 | "@babel/preset-react": "^7.16.7",
34 | "@babel/preset-typescript": "^7.16.7",
35 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
36 | "@storybook/addon-actions": "^6.5.12",
37 | "@storybook/addon-essentials": "^6.5.12",
38 | "@storybook/addon-interactions": "^6.5.12",
39 | "@storybook/addon-links": "^6.5.12",
40 | "@storybook/builder-webpack5": "^6.5.12",
41 | "@storybook/manager-webpack5": "^6.5.12",
42 | "@storybook/react": "^6.5.12",
43 | "@storybook/testing-library": "^0.0.13",
44 | "@svgr/webpack": "^6.2.1",
45 | "@testing-library/jest-dom": "^5.16.2",
46 | "@testing-library/react": "^12.1.3",
47 | "@types/jest": "^27.4.1",
48 | "@types/node": "^17.0.21",
49 | "@types/react": "^17.0.39",
50 | "@types/react-dom": "^17.0.11",
51 | "@types/react-router-dom": "^5.3.3",
52 | "@types/webpack": "^5.28.0",
53 | "@types/webpack-bundle-analyzer": "^4.4.1",
54 | "@types/webpack-dev-server": "^4.7.2",
55 | "@typescript-eslint/eslint-plugin": "^5.38.0",
56 | "babel-loader": "^8.2.3",
57 | "babel-plugin-i18next-extract": "^0.8.3",
58 | "css-loader": "^6.6.0",
59 | "eslint": "^8.24.0",
60 | "eslint-config-standard-with-typescript": "^23.0.0",
61 | "eslint-plugin-i18next": "^5.1.2",
62 | "eslint-plugin-import": "^2.26.0",
63 | "eslint-plugin-n": "^15.3.0",
64 | "eslint-plugin-promise": "^6.0.1",
65 | "eslint-plugin-react": "^7.31.8",
66 | "eslint-plugin-react-hooks": "^4.3.0",
67 | "file-loader": "^6.2.0",
68 | "html-webpack-plugin": "^5.5.0",
69 | "husky": "^8.0.0",
70 | "identity-obj-proxy": "^3.0.0",
71 | "jest": "^27.5.1",
72 | "json-server": "^0.17.1",
73 | "loki": "^0.30.3",
74 | "mini-css-extract-plugin": "^2.5.3",
75 | "react-refresh": "^0.14.0",
76 | "reg-cli": "^0.17.6",
77 | "regenerator-runtime": "^0.13.9",
78 | "sass": "^1.49.9",
79 | "sass-loader": "^12.6.0",
80 | "style-loader": "^3.3.1",
81 | "stylelint": "^14.5.3",
82 | "stylelint-config-standard-scss": "^3.0.0",
83 | "ts-loader": "^9.2.6",
84 | "ts-node": "^10.8.1",
85 | "typescript": "^4.8.3",
86 | "webpack": "^5.69.1",
87 | "webpack-bundle-analyzer": "^4.5.0",
88 | "webpack-cli": "^4.10.0",
89 | "webpack-dev-server": "^4.7.4"
90 | },
91 | "dependencies": {
92 | "@reduxjs/toolkit": "^1.8.0",
93 | "axios": "^1.1.3",
94 | "i18next": "^21.6.11",
95 | "i18next-browser-languagedetector": "^6.1.3",
96 | "i18next-http-backend": "^1.3.2",
97 | "react": "^17.0.2",
98 | "react-dom": "^17.0.2",
99 | "react-i18next": "^11.15.5",
100 | "react-redux": "^7.2.6",
101 | "react-router-dom": "^6.2.1"
102 | },
103 | "loki": {
104 | "configurations": {
105 | "chrome.laptop": {
106 | "target": "chrome.app",
107 | "width": 1366,
108 | "height": 768,
109 | "deviceScaleFactor": 1,
110 | "mobile": false
111 | },
112 | "chrome.iphone7": {
113 | "target": "chrome.app",
114 | "preset": "iPhone 7"
115 | }
116 | }
117 | },
118 | "overrides": {
119 | "chrome-remote-interface": "0.31.3"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/config/jest/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | import path from 'path';
7 |
8 | export default {
9 | globals: {
10 | __IS_DEV__: true
11 | },
12 | clearMocks: true,
13 | testEnvironment: 'jsdom',
14 | coveragePathIgnorePatterns: ['\\\\node_modules\\\\'],
15 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
16 | moduleDirectories: ['node_modules'],
17 | modulePaths: ['src'],
18 | testMatch: ['src/**/*(*.)@(spec|test).[tj]s?(x)'],
19 | rootDir: '../../',
20 | setupFilesAfterEnv: ['config/jest/setupTests.ts'],
21 | moduleNameMapper: {
22 | '\\.s?css$': 'identity-obj-proxy',
23 | '\\.svg': path.resolve(__dirname, 'jestEmptyComponent.tsx')
24 | },
25 | transformIgnorePatterns: ['node_modules/(?!axios)']
26 | // Indicates whether the coverage information should be collected while executing the test
27 | // collectCoverage: false,
28 |
29 | // An array of glob patterns indicating a set of files for which coverage information should be collected
30 | // collectCoverageFrom: undefined,
31 |
32 | // The directory where Jest should output its coverage files
33 | // coverageDirectory: undefined,
34 |
35 | // An array of regexp pattern strings used to skip coverage collection
36 |
37 | // Indicates which provider should be used to instrument code for coverage
38 | // coverageProvider: "babel",
39 |
40 | // A list of reporter names that Jest uses when writing coverage reports
41 | // coverageReporters: [
42 | // "json",
43 | // "text",
44 | // "lcov",
45 | // "clover"
46 | // ],
47 |
48 | // An object that configures minimum threshold enforcement for coverage results
49 | // coverageThreshold: undefined,
50 |
51 | // A path to a custom dependency extractor
52 | // dependencyExtractor: undefined,
53 |
54 | // Make calling deprecated APIs throw helpful error messages
55 | // errorOnDeprecated: false,
56 |
57 | // Force coverage collection from ignored files using an array of glob patterns
58 | // forceCoverageMatch: [],
59 |
60 | // A path to a module which exports an async function that is triggered once before all test suites
61 | // globalSetup: undefined,
62 |
63 | // A path to a module which exports an async function that is triggered once after all test suites
64 | // globalTeardown: undefined,
65 |
66 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
67 | // maxWorkers: "50%",
68 |
69 | // An array of directory names to be searched recursively up from the requiring module's location
70 |
71 | // An array of file extensions your modules use
72 |
73 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
74 | // moduleNameMapper: {},
75 |
76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
77 | // modulePathIgnorePatterns: [],
78 |
79 | // Activates notifications for test results
80 | // notify: false,
81 |
82 | // An enum that specifies notification mode. Requires { notify: true }
83 | // notifyMode: "failure-change",
84 |
85 | // A preset that is used as a base for Jest's configuration
86 | // preset: undefined,
87 |
88 | // Run tests from one or more projects
89 | // projects: undefined,
90 |
91 | // Use this configuration option to add custom reporters to Jest
92 | // reporters: undefined,
93 |
94 | // Automatically reset mock state before every test
95 | // resetMocks: false,
96 |
97 | // Reset the module registry before running each individual test
98 | // resetModules: false,
99 |
100 | // A path to a custom resolver
101 | // resolver: undefined,
102 |
103 | // Automatically restore mock state and implementation before every test
104 | // restoreMocks: false,
105 |
106 | // The root directory that Jest should scan for tests and modules within
107 |
108 | // A list of paths to directories that Jest should use to search for files in
109 | // roots: [
110 | // ""
111 | // ],
112 |
113 | // Allows you to use a custom runner instead of Jest's default test runner
114 | // runner: "jest-runner",
115 |
116 | // The paths to modules that run some code to configure or set up the testing environment before each test
117 | // setupFiles: [],
118 |
119 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
120 | // setupFilesAfterEnv: [],
121 |
122 | // The number of seconds after which a test is considered as slow and reported as such in the results.
123 | // slowTestThreshold: 5,
124 |
125 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
126 | // snapshotSerializers: [],
127 |
128 | // The test environment that will be used for testing
129 |
130 | // Options that will be passed to the testEnvironment
131 | // testEnvironmentOptions: {},
132 |
133 | // Adds a location field to test results
134 | // testLocationInResults: false,
135 |
136 | // The glob patterns Jest uses to detect test files
137 |
138 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
139 | // testPathIgnorePatterns: [
140 | // "\\\\node_modules\\\\"
141 | // ],
142 |
143 | // The regexp pattern or array of patterns that Jest uses to detect test files
144 | // testRegex: [],
145 |
146 | // This option allows the use of a custom results processor
147 | // testResultsProcessor: undefined,
148 |
149 | // This option allows use of a custom test runner
150 | // testRunner: "jest-circus/runner",
151 |
152 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
153 | // testURL: "http://localhost",
154 |
155 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
156 | // timers: "real",
157 |
158 | // A map from regular expressions to paths to transformers
159 | // transform: undefined,
160 |
161 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
162 |
163 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
164 | // unmockedModulePathPatterns: undefined,
165 |
166 | // Indicates whether each individual test should be reported during the run
167 | // verbose: undefined,
168 |
169 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
170 | // watchPathIgnorePatterns: [],
171 |
172 | // Whether to use watchman for file crawling
173 | // watchman: true,
174 | };
175 |
--------------------------------------------------------------------------------