├── 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 |
11 |
12 |
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 |
19 | 20 | 21 |
22 | 23 | 24 |
25 |
26 |
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 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/shared/assets/icons/theme-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 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 | 2 | 3 | 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) => 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 | --------------------------------------------------------------------------------