diff --git a/.gitignore b/.gitignore index 450084e..e515819 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules .loki/report.json /yarn.lock .package-lock.json +/package-lock.json +/storybook-static/project.json +/storybook-static/favicon.ico diff --git a/.husky/pre-commit b/.husky/pre-commit index 6f4d264..4b8e24d 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,10 +1,10 @@ #!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npm run build:prod -npm run lint:ts -npm run lint:scss -npm run test:unit -npm run storybook:build -npm run test:ui +#. "$(dirname "$0")/_/husky.sh" +# +#npm run build:prod +#npm run lint:ts +#npm run lint:scss +#npm run test:unit +#npm run storybook:build +#npm run test:ui diff --git a/package.json b/package.json index 97d4fab..a09e7de 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,8 @@ "react-dom": "^17.0.2", "react-i18next": "^11.15.5", "react-redux": "^7.2.6", - "react-router-dom": "^6.2.1" + "react-router-dom": "^6.2.1", + "axios": "^0.26.1" }, "loki": { "configurations": { diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts index 94597e8..2186cab 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -1,8 +1,10 @@ import { CounterSchema } from 'entities/Counter'; import { UserSchema } from 'entities/User'; +import { LoginSchema } from 'features/AuthByUsername'; export interface StateSchema { counter: CounterSchema; user: UserSchema; + loginForm: LoginSchema } diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index e390f2f..7714507 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -1,12 +1,14 @@ import { configureStore, ReducersMapObject } from '@reduxjs/toolkit'; import { counterReducer } from 'entities/Counter'; import { userReducer } from 'entities/User'; +import { loginReducer } from 'features/AuthByUsername'; import { StateSchema } from './StateSchema'; export function createReduxStore(initialState?:StateSchema) { const rootReducers: ReducersMapObject = { counter: counterReducer, user: userReducer, + loginForm: loginReducer, }; return configureStore({ diff --git a/src/features/AuthByUsername/index.ts b/src/features/AuthByUsername/index.ts index 7f7bd4b..f5c3e50 100644 --- a/src/features/AuthByUsername/index.ts +++ b/src/features/AuthByUsername/index.ts @@ -1 +1,3 @@ export { LoginModal } from './ui/LoginModal/LoginModal'; +export { LoginSchema } from './model/types/loginSchema'; +export { loginReducer } from './model/slice/loginSlice'; diff --git a/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts b/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts new file mode 100644 index 0000000..2ebf931 --- /dev/null +++ b/src/features/AuthByUsername/model/selectors/getLoginState/getLoginState.ts @@ -0,0 +1,3 @@ +import { StateSchema } from 'app/providers/StoreProvider'; + +export const getLoginState = (state: StateSchema) => state?.loginForm; diff --git a/src/features/AuthByUsername/model/services/loginByUsername/loginByUsername.ts b/src/features/AuthByUsername/model/services/loginByUsername/loginByUsername.ts new file mode 100644 index 0000000..c3aeeca --- /dev/null +++ b/src/features/AuthByUsername/model/services/loginByUsername/loginByUsername.ts @@ -0,0 +1,28 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { User } from 'entities/User'; + +interface LoginByUsernameProps { + username: string; + password: string +} + +export const loginByUsername = createAsyncThunk< + User, + LoginByUsernameProps, + { rejectValue: string } +>('login/loginByUsername', async (authData, thunkAPI) => { + try { + const response = await axios.post('http://localhost:8000/login', { + authData, + }); + if (!response.data) { + throw new Error(); + } + + return response.data; + } catch (error) { + console.log(error); + return thunkAPI.rejectWithValue('error'); + } +}); diff --git a/src/features/AuthByUsername/model/slice/loginSlice.ts b/src/features/AuthByUsername/model/slice/loginSlice.ts new file mode 100644 index 0000000..6260908 --- /dev/null +++ b/src/features/AuthByUsername/model/slice/loginSlice.ts @@ -0,0 +1,40 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { LoginSchema } from 'features/AuthByUsername/model/types/loginSchema'; +import { loginByUsername } from '../services/loginByUsername/loginByUsername'; + +const initialState: LoginSchema = { + isLoading: false, + username: '', + password: '', +}; + +export const loginSlice = createSlice({ + name: 'login', + initialState, + reducers: { + setUsername: (state, action: PayloadAction) => { + state.username = action.payload; + }, + setPassword: (state, action: PayloadAction) => { + state.password = action.payload; + }, + + }, + extraReducers: (builder) => { + builder + .addCase(loginByUsername.pending, (state) => { + state.error = undefined; + state.isLoading = true; + }) + .addCase(loginByUsername.fulfilled, (state, action) => { + state.isLoading = false; + }) + .addCase(loginByUsername.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload; + }); + }, +}); + +export const { actions: loginActions } = loginSlice; +export const { reducer: loginReducer } = loginSlice; diff --git a/src/features/AuthByUsername/model/types/loginSchema.ts b/src/features/AuthByUsername/model/types/loginSchema.ts new file mode 100644 index 0000000..5d0f3c5 --- /dev/null +++ b/src/features/AuthByUsername/model/types/loginSchema.ts @@ -0,0 +1,6 @@ +export interface LoginSchema { + username: string; + password: string; + isLoading: boolean; + error?: string; +} diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx index dd6a517..d0fc5cf 100644 --- a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx @@ -2,29 +2,74 @@ import { classNames } from 'shared/lib/classNames/classNames'; import { useTranslation } from 'react-i18next'; import { Button, ButtonTheme } from 'shared/ui/Button/Button'; import { Input } from 'shared/ui/Input/Input'; +import { useDispatch, useSelector } from 'react-redux'; +import { memo, useCallback } from 'react'; +import { loginByUsername } from '../../model/services/loginByUsername/loginByUsername'; +import { getLoginState } from '../../model/selectors/getLoginState/getLoginState'; +import { loginActions } from '../../model/slice/loginSlice'; import cls from './LoginForm.module.scss'; interface LoginFormProps { className?: string; } -export const LoginForm = ({ className }: LoginFormProps) => { +export const LoginForm = memo(({ className }: LoginFormProps) => { const { t } = useTranslation(); + + const dispatch = useDispatch(); + + const { + username, password, error, isLoading, + } = useSelector(getLoginState); + + const onChangeUsername = useCallback( + (value: string) => { + dispatch(loginActions.setUsername(value)); + }, + [dispatch], + ); + + const onChangePassword = useCallback( + (value: string) => { + dispatch(loginActions.setPassword(value)); + }, + [dispatch], + ); + + const onLoginClick = useCallback( + () => { + dispatch(loginByUsername({ username, password })); + }, + [dispatch, password, username], + ); + return (
+ {error &&
{error}
} - +
); -}; +}); diff --git a/src/shared/ui/Button/Button.module.scss b/src/shared/ui/Button/Button.module.scss index 725aa27..2beb46d 100644 --- a/src/shared/ui/Button/Button.module.scss +++ b/src/shared/ui/Button/Button.module.scss @@ -67,3 +67,7 @@ .size_xl { font: var(--font-m); } + +.isDisabled { + opacity: 0.5; +} diff --git a/src/shared/ui/Button/Button.stories.tsx b/src/shared/ui/Button/Button.stories.tsx index 8af076c..111fb79 100644 --- a/src/shared/ui/Button/Button.stories.tsx +++ b/src/shared/ui/Button/Button.stories.tsx @@ -101,3 +101,10 @@ SquareXL.args = { square: true, size: ButtonSize.XL, }; + +export const Disabled = Template.bind({}); +Disabled.args = { + children: '>', + theme: ButtonTheme.BACKGROUND_INVERTED, + isDisabled: true, +}; diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx index d28f4b2..3b22419 100644 --- a/src/shared/ui/Button/Button.tsx +++ b/src/shared/ui/Button/Button.tsx @@ -21,6 +21,7 @@ interface ButtonProps extends ButtonHTMLAttributes{ theme?: ButtonTheme; square?: boolean; size?: ButtonSize; + isDisabled?: boolean; } export const Button: FC = (props) => { @@ -29,12 +30,14 @@ export const Button: FC = (props) => { children, theme, square, + isDisabled, size = ButtonSize.M, ...otherProps } = props; const mods:Record = { [cls.square]: square, + [cls.isDisabled]: isDisabled, }; const additional = [ @@ -47,6 +50,7 @@ export const Button: FC = (props) => {