diff --git a/.eslintrc.js b/.eslintrc.js index 14c0286..fc992e9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,6 +22,7 @@ module.exports = { rules: { 'prettier/prettier': 'error', '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', 'react/jsx-indent': [1, 2], 'react/jsx-indent-props': [1, 2], indent: [1, 2], @@ -44,8 +45,9 @@ module.exports = { ], 'import/no-extraneous-dependencies': 'off', 'no-underscore-dangle': 'off', + // временно отключено, чтобы не было ошибок при использовании i18next 'i18next/no-literal-string': [ - 'error', + 0, { markupOnly: true, ignoreAttribute: ['data-testid', 'to'], diff --git a/extractedTranslations/en/about.json b/extractedTranslations/en/about.json deleted file mode 100644 index 1fff65f..0000000 --- a/extractedTranslations/en/about.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "О сайте": "" -} diff --git a/json-server/db.json b/json-server/db.json index 6ebc62d..4511bb6 100644 --- a/json-server/db.json +++ b/json-server/db.json @@ -1,6 +1,6 @@ { - "posts": [{ "id": 1, "title": "json-server", "author": "typicode" }], - "comments": [{ "id": 1, "body": "some comment", "postId": 1 }], - "users": [{ "id": 1, "username": "admin", "password": "123" }], - "profile": { "name": "typicode" } + "posts": [{ "id": 1, "title": "json-server", "author": "typicode" }], + "comments": [{ "id": 1, "body": "some comment", "postId": 1 }], + "users": [{ "id": 1, "username": "admin", "password": "123" }], + "profile": { "name": "typicode" } } diff --git a/json-server/index.js b/json-server/index.js index b99f743..71d9359 100644 --- a/json-server/index.js +++ b/json-server/index.js @@ -1,8 +1,9 @@ /* eslint-disable no-console */ const fs = require('fs'); -const jsonServer = require('json-server'); const path = require('path'); +const jsonServer = require('json-server'); + const server = jsonServer.create(); const router = jsonServer.router(path.resolve(__dirname, 'db.json')); diff --git a/package.json b/package.json index 176ba35..2d99fe5 100644 --- a/package.json +++ b/package.json @@ -1,119 +1,119 @@ { - "name": "production-project", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "webpack serve --env port=3000", - "start:dev:server": "node ./json-server/index.js", - "build:prod": "webpack --env mode=production", - "build:dev": "webpack --env mode=development", - "lint:ts": "eslint \"**/*.{ts,tsx}\"", - "lint:ts:fix": "eslint \"**/*.{ts,tsx}\" --fix && prettier --write \"**/*.{ts,tsx}\"", - "lint:scss": "npx stylelint \"**/*.scss\"", - "lint:scss:fix": "npx stylelint \"**/*.scss\" --fix", - "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,scss,css,json}\"", - "format": "npm run lint:ts:fix && npm run lint:scss:fix", - "test:unit": "jest --config ./config/jest/jest.config.ts", - "test:ui": "npx loki test --reactUri=http://host.docker.internal:6006", - "test:ui:ok": "npx loki approve", - "test:ui:ci": "npx loki test --requireReference --reactUri=file:./storybook-static", - "test:ui:report": "npm run test:ui:json && npm run test:ui:html", - "test:ui:json": "node scripts/generate-visual-json-report.js", - "test:ui:html": "reg-cli --from .loki/report.json --report .loki/report.html", - "storybook": "start-storybook -p 6006 -c ./config/storybook", - "storybook:build": "build-storybook -c ./config/storybook", - "prepare": "husky install" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@babel/core": "^7.17.5", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", - "@babel/preset-typescript": "^7.16.7", - "@storybook/addon-actions": "^6.4.19", - "@storybook/addon-essentials": "^6.4.19", - "@storybook/addon-interactions": "^6.4.19", - "@storybook/addon-links": "^6.4.19", - "@storybook/builder-webpack5": "^6.4.19", - "@storybook/manager-webpack5": "^6.4.19", - "@storybook/react": "^6.4.19", - "@storybook/testing-library": "^0.0.9", - "@svgr/webpack": "^6.2.1", - "@testing-library/jest-dom": "^5.16.2", - "@testing-library/react": "^12.1.3", - "@types/jest": "^27.4.1", - "@types/lodash": "4.14.202", - "@types/node": "^17.0.21", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.11", - "@types/react-router-dom": "^5.3.3", - "@types/webpack": "^5.28.0", - "@types/webpack-bundle-analyzer": "^4.4.1", - "@types/webpack-dev-server": "^4.7.2", - "@typescript-eslint/eslint-plugin": "^5.12.1", - "@typescript-eslint/parser": "^5.12.1", - "babel-loader": "^8.2.3", - "babel-plugin-i18next-extract": "^0.8.3", - "css-loader": "^6.6.0", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^10.1.5", - "eslint-plugin-i18next": "^5.1.2", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^5.4.1", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.0", - "husky": "^8.0.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.5.1", - "json-server": "^0.17.0", - "loki": "^0.28.1", - "mini-css-extract-plugin": "^2.5.3", - "prettier": "^3.5.3", - "reg-cli": "^0.17.6", - "regenerator-runtime": "^0.13.9", - "sass": "^1.49.9", - "sass-loader": "^12.6.0", - "style-loader": "^3.3.1", - "stylelint": "^14.5.3", - "stylelint-config-standard-scss": "^3.0.0", - "ts-loader": "^9.2.6", - "ts-node": "^10.5.0", - "typescript": "^4.5.5", - "webpack": "^5.69.1", - "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" - }, - "dependencies": { - "@reduxjs/toolkit": "1.8", - "axios": "^1.9.0", - "i18next": "^21.6.11", - "i18next-browser-languagedetector": "^6.1.3", - "i18next-http-backend": "^1.3.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-i18next": "^11.15.5", - "react-redux": "^7.2.6", - "react-router-dom": "^6.2.1" - }, - "loki": { - "configurations": { - "chrome.laptop": { - "target": "chrome.docker", - "width": 1366, - "height": 768 - }, - "chrome.iphone7": { - "target": "chrome.docker", - "preset": "iPhone 7" - } - } - } + "name": "production-project", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "webpack serve --env port=3000", + "start:dev:server": "node ./json-server/index.js", + "build:prod": "webpack --env mode=production", + "build:dev": "webpack --env mode=development", + "lint:ts": "eslint \"**/*.{ts,tsx}\"", + "lint:ts:fix": "prettier --write \"**/*.{ts,tsx}\" && eslint \"**/*.{ts,tsx}\" --fix", + "lint:scss": "npx stylelint \"**/*.scss\"", + "lint:scss:fix": "npx stylelint \"**/*.scss\" --fix", + "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,scss,css,json}\"", + "format": "npm run lint:ts:fix && npm run lint:scss:fix", + "test:unit": "jest --config ./config/jest/jest.config.ts", + "test:ui": "npx loki test --reactUri=http://host.docker.internal:6006", + "test:ui:ok": "npx loki approve", + "test:ui:ci": "npx loki test --requireReference --reactUri=file:./storybook-static", + "test:ui:report": "npm run test:ui:json && npm run test:ui:html", + "test:ui:json": "node scripts/generate-visual-json-report.js", + "test:ui:html": "reg-cli --from .loki/report.json --report .loki/report.html", + "storybook": "start-storybook -p 6006 -c ./config/storybook", + "storybook:build": "build-storybook -c ./config/storybook", + "prepare": "husky install" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/core": "^7.17.5", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "@storybook/addon-actions": "^6.4.19", + "@storybook/addon-essentials": "^6.4.19", + "@storybook/addon-interactions": "^6.4.19", + "@storybook/addon-links": "^6.4.19", + "@storybook/builder-webpack5": "^6.4.19", + "@storybook/manager-webpack5": "^6.4.19", + "@storybook/react": "^6.4.19", + "@storybook/testing-library": "^0.0.9", + "@svgr/webpack": "^6.2.1", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^12.1.3", + "@types/jest": "^27.4.1", + "@types/lodash": "4.14.202", + "@types/node": "^17.0.21", + "@types/react": "^17.0.39", + "@types/react-dom": "^17.0.11", + "@types/react-router-dom": "^5.3.3", + "@types/webpack": "^5.28.0", + "@types/webpack-bundle-analyzer": "^4.4.1", + "@types/webpack-dev-server": "^4.7.2", + "@typescript-eslint/eslint-plugin": "^5.12.1", + "@typescript-eslint/parser": "^5.12.1", + "babel-loader": "^8.2.3", + "babel-plugin-i18next-extract": "^0.8.3", + "css-loader": "^6.6.0", + "eslint": "^8.10.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-i18next": "^5.1.2", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-react": "^7.29.2", + "eslint-plugin-react-hooks": "^4.3.0", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.0", + "husky": "^8.0.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.5.1", + "json-server": "^0.17.0", + "loki": "^0.28.1", + "mini-css-extract-plugin": "^2.5.3", + "prettier": "^3.5.3", + "reg-cli": "^0.17.6", + "regenerator-runtime": "^0.13.9", + "sass": "^1.49.9", + "sass-loader": "^12.6.0", + "style-loader": "^3.3.1", + "stylelint": "^14.5.3", + "stylelint-config-standard-scss": "^3.0.0", + "ts-loader": "^9.2.6", + "ts-node": "^10.5.0", + "typescript": "^4.5.5", + "webpack": "^5.69.1", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" + }, + "dependencies": { + "@reduxjs/toolkit": "1.8", + "axios": "^1.9.0", + "i18next": "^21.6.11", + "i18next-browser-languagedetector": "^6.1.3", + "i18next-http-backend": "^1.3.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-i18next": "^11.15.5", + "react-redux": "^7.2.6", + "react-router-dom": "^6.2.1" + }, + "loki": { + "configurations": { + "chrome.laptop": { + "target": "chrome.docker", + "width": 1366, + "height": 768 + }, + "chrome.iphone7": { + "target": "chrome.docker", + "preset": "iPhone 7" + } + } + } } diff --git a/src/app/providers/ErrorBoundary/ui/ErrorBoundary.tsx b/src/app/providers/ErrorBoundary/ui/ErrorBoundary.tsx index 70f474a..dd24016 100644 --- a/src/app/providers/ErrorBoundary/ui/ErrorBoundary.tsx +++ b/src/app/providers/ErrorBoundary/ui/ErrorBoundary.tsx @@ -16,6 +16,7 @@ class ErrorBoundary extends React.Component ReducersMapObject; + reduce: (state: StateSchema, action: AnyAction) => CombinedState; + add: (key: StateSchemaKey, reducer: Reducer) => void; + remove: (key: StateSchemaKey) => void; +} + +export interface StoreWithManager extends EnhancedStore { + reducerManager: ReducerManager; +} diff --git a/src/app/providers/StoreProvider/config/reducerManager.ts b/src/app/providers/StoreProvider/config/reducerManager.ts new file mode 100644 index 0000000..77e6873 --- /dev/null +++ b/src/app/providers/StoreProvider/config/reducerManager.ts @@ -0,0 +1,44 @@ +import type { AnyAction, Reducer, ReducersMapObject } from '@reduxjs/toolkit'; +import { combineReducers } from '@reduxjs/toolkit'; + +import type { ReducerManager, StateSchema, StateSchemaKey } from './StateSchema'; + +export function createReducerManager( + initialReducers: ReducersMapObject, +): ReducerManager { + const reducers = { ...initialReducers }; + + let combinedReducer = combineReducers(reducers); + + let keysToRemove: StateSchemaKey[] = []; + + return { + getReducerMap: () => reducers, + reduce: (state: StateSchema, action: AnyAction) => { + if (keysToRemove.length > 0) { + state = { ...state }; + keysToRemove.forEach((key) => { + delete state[key]; + }); + keysToRemove = []; + } + return combinedReducer(state, action); + }, + add: (key: StateSchemaKey, reducer: Reducer) => { + if (!key || reducers[key]) { + return; + } + reducers[key] = reducer; + + combinedReducer = combineReducers(reducers); + }, + remove: (key: StateSchemaKey) => { + if (!key || !reducers[key]) { + return; + } + delete reducers[key]; + keysToRemove.push(key); + combinedReducer = combineReducers(reducers); + }, + }; +} diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index d3cc334..a3f2d96 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -2,20 +2,26 @@ import type { ReducersMapObject } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit'; import { counterReducer } from 'entities/Counter'; import { userReducer } from 'entities/User'; -import { loginReducer } from 'features/AuthByUsername'; import type { StateSchema } from './StateSchema'; +import { createReducerManager } from './reducerManager'; export function createReduxStore(initialState?: StateSchema) { const rootReducers: ReducersMapObject = { counter: counterReducer, user: userReducer, - loginForm: loginReducer, }; - return configureStore({ - reducer: rootReducers, + const reducerManager = createReducerManager(rootReducers); + + const store = configureStore({ + reducer: reducerManager.reduce, devTools: __IS_DEV__, preloadedState: initialState, }); + + // @ts-ignore + store.reducerManager = reducerManager; + + return store; } diff --git a/src/app/providers/StoreProvider/index.ts b/src/app/providers/StoreProvider/index.ts index 5d6cfa6..a5ea165 100644 --- a/src/app/providers/StoreProvider/index.ts +++ b/src/app/providers/StoreProvider/index.ts @@ -1,3 +1,3 @@ -export type { StateSchema } from './config/StateSchema'; +export type { StateSchema, StoreWithManager } from './config/StateSchema'; export { createReduxStore } from './config/store'; export { StoreProvider } from './ui/StoreProvider'; diff --git a/src/app/styles/variables/global.scss b/src/app/styles/variables/global.scss index 2a4b064..9167c70 100644 --- a/src/app/styles/variables/global.scss +++ b/src/app/styles/variables/global.scss @@ -1,5 +1,14 @@ :root { - --font-family-main: nunito, "Times New Roman", serif; + --font-family-main: + "Montserrat", + "Segoe UI", + "Arial", + "Helvetica Neue", + "Times New Roman", + "Georgia", + "Trebuchet MS", + "Verdana", + serif; --font-size-m: 16px; --font-line-m: 24px; --font-m: var(--font-size-m) / var(--font-line-m) var(--font-family-main); diff --git a/src/app/types/global.d.ts b/src/app/types/global.d.ts index 92dc27b..0b2cfe7 100644 --- a/src/app/types/global.d.ts +++ b/src/app/types/global.d.ts @@ -17,5 +17,4 @@ declare module '*.svg' { export default SVG; } -// eslint-disable-next-line no-unused-vars declare const __IS_DEV__: boolean; diff --git a/src/features/AuthByUsername/index.ts b/src/features/AuthByUsername/index.ts index 23670a8..ad1b59b 100644 --- a/src/features/AuthByUsername/index.ts +++ b/src/features/AuthByUsername/index.ts @@ -1,3 +1,2 @@ export { LoginModal } from './ui/LoginModal/LoginModal'; export type { LoginSchema } from './model/types/loginSchema'; -export { loginReducer } from './model/slice/loginSlice'; diff --git a/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts b/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts new file mode 100644 index 0000000..b387b90 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginError/getLoginError.ts @@ -0,0 +1,3 @@ +import type { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginError = (state: StateSchema) => state?.loginForm?.error; diff --git a/src/features/AuthByUsername/model/selectors/getLoginLoading/getLoginLoading.ts b/src/features/AuthByUsername/model/selectors/getLoginLoading/getLoginLoading.ts new file mode 100644 index 0000000..639301c --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginLoading/getLoginLoading.ts @@ -0,0 +1,3 @@ +import type { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginLoading = (state: StateSchema) => state?.loginForm?.isLoading; diff --git a/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts b/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts new file mode 100644 index 0000000..d481fc6 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginPassword/getLoginPassword.ts @@ -0,0 +1,3 @@ +import type { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginPassword = (state: StateSchema) => state?.loginForm?.password; diff --git a/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts b/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts deleted file mode 100644 index 3f8fb64..0000000 --- a/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { StateSchema } from 'app/providers/StoreProvider'; - -export const getLoginState = (state: StateSchema) => state.loginForm; diff --git a/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts b/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts new file mode 100644 index 0000000..3545192 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginUsername/getLoginUsername.ts @@ -0,0 +1,3 @@ +import type { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginUsername = (state: StateSchema) => state?.loginForm?.username; diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts new file mode 100644 index 0000000..652d709 --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.async.ts @@ -0,0 +1,13 @@ +import type { FC } from 'react'; +import { lazy } from 'react'; + +import type { LoginFormProps } from './LoginForm'; + +export const LoginFormAsync = lazy>( + () => + new Promise((resolve) => { + // @ts-ignore + // ТАК В РЕАЛЬНЫХ ПРОЕКТАХ НЕ ДЕЛАТЬ!!!!! ДЕЛАЕМ ДЛЯ КУРСА! + setTimeout(() => resolve(import('./LoginForm')), 1500); + }), +); diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx index 6c977a4..441e196 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.stories.tsx @@ -3,7 +3,7 @@ import { Theme } from 'app/providers/ThemeProvider'; import { StoreDecorator } from 'shared/config/storybook/StoreDecorator/StoreDecorator'; import { ThemeDecorator } from 'shared/config/storybook/ThemeDecorator/ThemeDecorator'; -import { LoginForm } from './LoginForm'; +import LoginForm from './LoginForm'; export default { title: 'features/LoginForm', diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx index 969a75a..ff48b91 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx @@ -1,28 +1,52 @@ import { loginByUsername } from 'features/AuthByUsername/model/services/loginByUsername/loginByUsername'; import type { FormEvent, KeyboardEvent } from 'react'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector, useStore } from 'react-redux'; import { classNames } from 'shared/lib/classNames/classNames'; import { Button, ButtonTheme } from 'shared/ui/Button/Button'; import { Input } from 'shared/ui/Input/Input'; import { Text, TextAlign, TextSize, TextTheme } from 'shared/ui/Text/Text'; +import type { StoreWithManager } from 'app/providers/StoreProvider'; +import i18n from 'shared/config/i18n/i18n'; +import type { ReducerList } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader'; +import { DynamicModuleLoader } from 'shared/lib/components/DynamicModuleLoader/DynamicModuleLoader'; -import { getLoginState } from '../../model/selectors/getLoginState/getLoginState'; -import { loginActions } from '../../model/slice/loginSlice'; +import { getLoginError } from '../../model/selectors/getLoginError/getLoginError'; +import { getLoginLoading } from '../../model/selectors/getLoginLoading/getLoginLoading'; +import { getLoginPassword } from '../../model/selectors/getLoginPassword/getLoginPassword'; +import { getLoginUsername } from '../../model/selectors/getLoginUsername/getLoginUsername'; +import { loginActions, loginReducer } from '../../model/slice/loginSlice'; import styles from './LoginForm.module.scss'; -// eslint-disable-next-line import/order -import i18n from 'shared/config/i18n/i18n'; - -interface LoginFormProps { +export interface LoginFormProps { className?: string; } -export const LoginForm = memo(({ className }: LoginFormProps) => { +const initialReducers: ReducerList = { + loginForm: loginReducer, +}; + +const LoginForm = memo(({ className }: LoginFormProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const { username, password, isLoading, error } = useSelector(getLoginState); + + const store = useStore() as StoreWithManager; + + const username = useSelector(getLoginUsername); + const password = useSelector(getLoginPassword); + const isLoading = useSelector(getLoginLoading); + const error = useSelector(getLoginError); + + useEffect(() => { + store.reducerManager.add('loginForm', loginReducer); + dispatch({ type: 'login/init' }); + return () => { + store.reducerManager.remove('loginForm'); + dispatch({ type: 'login/reset' }); + }; + // eslint-disable-next-line + }, []); const onChangeUsername = useCallback( (value: string) => { @@ -60,46 +84,50 @@ export const LoginForm = memo(({ className }: LoginFormProps) => { ); return ( -
- - {error && ( - - )} - - - - + + {error && ( + + )} + + + + + ); }); + +export default LoginForm; diff --git a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx index af16165..86f8378 100644 --- a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx +++ b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx @@ -1,7 +1,9 @@ import { classNames } from 'shared/lib/classNames/classNames'; import { Modal } from 'shared/ui/Modal/Modal'; +import { Suspense } from 'react'; +import { Loader } from 'shared/ui/Loader/Loader'; -import { LoginForm } from '../LoginForm/LoginForm'; +import { LoginFormAsync } from '../LoginForm/LoginForm.async'; interface LoginModalProps { className?: string; @@ -11,6 +13,8 @@ interface LoginModalProps { export const LoginModal = ({ className, isOpen, onClose }: LoginModalProps) => ( - + }> + + ); diff --git a/src/index.tsx b/src/index.tsx index 88b322e..9f05bcf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,10 +4,9 @@ import { ThemeProvider } from 'app/providers/ThemeProvider'; import { StoreProvider } from 'app/providers/StoreProvider'; import App from './app/App'; +import { ErrorBoundary } from './app/providers/ErrorBoundary'; import 'app/styles/index.scss'; - import './shared/config/i18n/i18n'; -import { ErrorBoundary } from './app/providers/ErrorBoundary'; render( diff --git a/src/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader.tsx b/src/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader.tsx new file mode 100644 index 0000000..d67e02b --- /dev/null +++ b/src/shared/lib/components/DynamicModuleLoader/DynamicModuleLoader.tsx @@ -0,0 +1,42 @@ +import type { Reducer } from '@reduxjs/toolkit'; +import type { FC } from 'react'; +import { useEffect } from 'react'; +import { useDispatch, useStore } from 'react-redux'; +import type { StoreWithManager } from 'app/providers/StoreProvider'; +import type { StateSchemaKey } from 'app/providers/StoreProvider/config/StateSchema'; + +export type ReducerList = { + [name in StateSchemaKey]?: Reducer; +}; + +type ReducerListEntry = [StateSchemaKey, Reducer]; + +interface DynamicModuleLoaderProps { + reducers: ReducerList; + removeAfterUnmount?: boolean; +} + +export const DynamicModuleLoader: FC = (props) => { + const { children, reducers, removeAfterUnmount = true } = props; + const store = useStore() as StoreWithManager; + const dispatch = useDispatch(); + + useEffect(() => { + Object.entries(reducers).forEach(([name, reducer]: ReducerListEntry) => { + store.reducerManager.add(name, reducer); + dispatch({ type: `@INIT ${name} reducer` }); + }); + + return () => { + if (removeAfterUnmount) { + Object.entries(reducers).forEach(([name]: ReducerListEntry) => { + store.reducerManager.remove(name); + dispatch({ type: `@DESTROY ${name} reducer` }); + }); + } + }; + }, [dispatch, reducers, removeAfterUnmount, store.reducerManager]); + + // eslint-disable-next-line + return <>{children}; +}; diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx index df563ee..3368ea6 100644 --- a/src/shared/ui/Button/Button.tsx +++ b/src/shared/ui/Button/Button.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import type { ButtonHTMLAttributes, FC } from 'react'; import { classNames } from 'shared/lib/classNames/classNames'; diff --git a/src/shared/ui/Input/Input.tsx b/src/shared/ui/Input/Input.tsx index 0256dc6..67bf29e 100644 --- a/src/shared/ui/Input/Input.tsx +++ b/src/shared/ui/Input/Input.tsx @@ -10,7 +10,6 @@ interface InputProps extends HTMLInputProps { className?: string; value?: string | number; autoFocus?: boolean; - // eslint-disable-next-line no-unused-vars onChange?: (value: string) => void; } diff --git a/src/widgets/Navbar/ui/Navbar.tsx b/src/widgets/Navbar/ui/Navbar.tsx index 1496754..bd56811 100644 --- a/src/widgets/Navbar/ui/Navbar.tsx +++ b/src/widgets/Navbar/ui/Navbar.tsx @@ -46,7 +46,7 @@ export const Navbar = ({ className }: NavbarProps) => { - + {isAuthModal && } ); };