diff --git a/package-lock.json b/package-lock.json index ea67dcbb..3dcbc26c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-loader-spinner": "^5.4.5", + "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", + "react-toastify": "^9.1.3", "redux-persist": "^6.0.0", "styled-components": "^6.0.7", "yup": "^1.2.0" @@ -3116,6 +3118,15 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3140,7 +3151,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -3163,6 +3174,11 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@vitejs/plugin-react": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.4.tgz", @@ -6263,6 +6279,49 @@ "react-is": ">= 16.8.0" } }, + "node_modules/react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -6302,6 +6361,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -7223,6 +7294,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", @@ -9353,6 +9432,15 @@ "react-window": "1.8.7" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -9377,7 +9465,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dev": true, + "devOptional": true, "requires": { "@types/react": "*" } @@ -9400,6 +9488,11 @@ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@vitejs/plugin-react": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.4.tgz", @@ -11595,6 +11688,26 @@ } } }, + "react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -11618,6 +11731,14 @@ "react-router": "6.15.0" } }, + "react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -12259,6 +12380,12 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "vite": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", diff --git a/package.json b/package.json index f91c753d..65238afa 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-loader-spinner": "^5.4.5", + "react-redux": "^8.1.2", "react-router-dom": "^6.15.0", + "react-toastify": "^9.1.3", "redux-persist": "^6.0.0", "styled-components": "^6.0.7", "yup": "^1.2.0" diff --git a/src/components/AuthForm/AuthForm.jsx b/src/components/AuthForm/AuthForm.jsx index a37c6c75..3965c324 100644 --- a/src/components/AuthForm/AuthForm.jsx +++ b/src/components/AuthForm/AuthForm.jsx @@ -1,8 +1,9 @@ import PropTypes from 'prop-types'; import * as Yup from 'yup'; import { Formik, ErrorMessage } from 'formik'; -import sprite from '../../assets/sprite.svg'; +import { useState } from 'react'; +import sprite from '../../assets/sprite.svg'; import { TextInput, FormContainer, @@ -13,27 +14,31 @@ import { Warning, } from './AuthForm.styled'; import AuthButton from '../AuthButton'; -import { useState } from 'react'; -const authSchema = Yup.object().shape({ - name: Yup.string().required('Name is required'), - email: Yup.string() - .matches(/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/, { - message: 'Email must be valid', - }) - .email('Invalid email') - .required('Email is required'), - password: Yup.string() - .matches(/^(&=.*[a-zA-Z]{6})(?=.*\d)[a-zA-Z\d]{7}$/, { - message: 'password must have ...', - }) - .required('Password is required'), -}); +const emailLyout = /^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/; +const passwordLayout = /^(?=.*[a-zA-Z]{6,})(?=.*\d)[a-zA-Z\d]{7,}$/; -export default function AuthForm({ nameIsShown, btnTitle }) { +export default function AuthForm({ nameIsShown, btnTitle, onSubmit }) { const [isPasswordShown, setIsPasswordShown] = useState(false); const [typePasswordInput, setTypePasswordInput] = useState('password'); + const validateName = nameIsShown => { + return nameIsShown ? Yup.string().required('Name is required') : null; + }; + const authSchema = Yup.object().shape({ + name: validateName(nameIsShown), + email: Yup.string() + .matches(emailLyout, { message: 'Email must be valid' }) + .email('Invalid email') + .required('Email is required'), + password: Yup.string() + .matches(passwordLayout, { + message: + 'Password must contain 6+ letters, 1 number, and 1+ letter or number', + }) + .required('Password is required'), + }); + const initialValues = nameIsShown ? { name: '', @@ -59,9 +64,7 @@ export default function AuthForm({ nameIsShown, btnTitle }) { return ( { - console.log(values); - }} + onSubmit={onSubmit} validationSchema={authSchema} > @@ -110,7 +113,7 @@ export default function AuthForm({ nameIsShown, btnTitle }) { placeholder="password" name="password" /> - + - - - + + + + + + + , ); diff --git a/src/pages/SignIn/SignIn.jsx b/src/pages/SignIn/SignIn.jsx index 09ee7658..d8a795e8 100644 --- a/src/pages/SignIn/SignIn.jsx +++ b/src/pages/SignIn/SignIn.jsx @@ -4,8 +4,17 @@ import Header from '../../components/headersComp/Header/Header'; import AuthForm from '../../components/AuthForm/AuthForm'; import BtnSubtitle from '../../components/BtnSubtitle/BtnSubtitle'; import { Wrapper } from '../Home/Home.styled'; +import { useDispatch } from 'react-redux'; +import { logInUser } from '../../redux/auth/operation'; const SignIn = () => { + const dispatch = useDispatch(); + + const logIn = (user, { resetForm }) => { + dispatch(logInUser(user)); + resetForm(); + }; + return (
@@ -15,7 +24,7 @@ const SignIn = () => { 'Welcome! Please enter your credentials to login to the platform:' } /> - + { + const dispatch = useDispatch(); + const handleSubmit = (user, { resetForm }) => { + console.log(user); + dispatch(authUser(user)); + resetForm(); + }; + return (
@@ -15,7 +24,8 @@ const SignUp = () => { 'Thank you for your interest in our platform. To complete the registration process, please provide us with the following information.' } /> - + + { + axios.defaults.headers.common.Authorization = `Bearer ${token}`; + }, + unSet: () => { + axios.defaults.headers.common.Authorization = ''; + }, +}; + +export const authUser = createAsyncThunk( + 'addUserStatus', + async (user, { rejectWithValue }) => { + try { + const { data } = await axios.post('/api/users/register', user); + console.log(data); + token.set(data.token); + return data; + } catch (error) { + toast.error('Oops... Something went wrong! Try again!'); + return rejectWithValue('Oops... Something went wrong!'); + } + }, +); + +export const logInUser = createAsyncThunk( + 'logInStatus', + async (user, { rejectWithValue }) => { + try { + const { data } = await axios.post('/api/users/login', user); + console.log(data); + token.set(data.token); + return data; + } catch (error) { + toast.error( + 'Oops... Something went wrong! Enter correct"email" and "password" or sign up, please', + ); + return rejectWithValue( + 'Oops... Something went wrong! Enter correct"email" and "password", please', + ); + } + }, +); + +export const logOutUser = createAsyncThunk( + 'logOutStatus', + async (_, { rejectWithValue }) => { + try { + await axios.post('/api/users/logout'); + console.log('qwe'); + token.unSet(); + } catch (error) { + toast.error('Oops, something went wrong((( Try again, please!'); + return rejectWithValue( + 'Oops, something went wrong((( Try again, please!', + ); + } + }, +); + +export const fetchCurrentUser = createAsyncThunk( + 'refreshUser', + async (_, { rejectWithValue, getState }) => { + const state = getState(); + const persistedToken = state.user.token; + if (!persistedToken) { + return rejectWithValue(); + } + token.set(persistedToken); + try { + const { data } = await axios.get('/api/users/current'); + + return data; + } catch (error) { + return rejectWithValue(error); + } + }, +); diff --git a/src/redux/auth/slice.js b/src/redux/auth/slice.js index e69de29b..1b63f95a 100644 --- a/src/redux/auth/slice.js +++ b/src/redux/auth/slice.js @@ -0,0 +1,82 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { authUser, fetchCurrentUser, logInUser, logOutUser } from './operation'; + +const initialState = { + user: { + name: null, + email: null, + avatarURL: null, + }, + error: null, + token: null, + isLogedIn: false, + isRefreshing: false, +}; + +export const authSlice = createSlice({ + name: 'authorization', + initialState, + reducers: {}, + extraReducers: builder => { + builder.addCase(authUser.fulfilled, (state, action) => { + state.token = action.payload.token; + state.isLogedIn = true; + state.error = null; + }); + builder.addCase(authUser.rejected, (state, action) => { + state.error = action.payload; + }); + builder.addCase(authUser.pending, state => { + state.isRefreshing = true; + }); + + builder.addCase(logInUser.fulfilled, (state, action) => { + state.user.name = action.payload.name; + state.user.email = action.payload.email; + state.user.avatarURL = action.payload.avatarURL; + state.token = action.payload.token; + state.isLogedIn = true; + state.error = null; + }); + builder.addCase(logInUser.rejected, (state, action) => { + state.error = action.payload; + }); + builder.addCase(logInUser.pending, state => { + state.isRefreshing = true; + }); + + builder.addCase(logOutUser.fulfilled, state => { + state.user.name = null; + state.user.email = null; + state.user.avatarURL = null; + state.token = null; + state.isLogedIn = false; + }); + builder.addCase(logOutUser.rejected, (state, action) => { + state.error = action.payload; + state.user.name = null; + state.user.email = null; + state.user.avatarURL = null; + state.token = null; + state.isLogedIn = false; + }); + builder.addCase(logOutUser.pending, state => { + state.isRefreshing = true; + }); + + builder.addCase(fetchCurrentUser.fulfilled, (state, action) => { + state.user.name = action.payload.name; + state.user.email = action.payload.email; + state.user.avatarURL = action.payload.avatarURL; + state.token = action.payload.token; + state.isLogedIn = true; + state.isRefreshing = false; + }); + builder.addCase(fetchCurrentUser.rejected, state => { + state.isRefreshing = false; + }); + builder.addCase(fetchCurrentUser.pending, state => { + state.isRefreshing = true; + }); + }, +}); diff --git a/src/redux/store.js b/src/redux/store.js new file mode 100644 index 00000000..d6ba5519 --- /dev/null +++ b/src/redux/store.js @@ -0,0 +1,36 @@ +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { + persistStore, + persistReducer, + FLUSH, + REHYDRATE, + PAUSE, + PERSIST, + PURGE, + REGISTER, +} from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; + +import { authSlice } from './auth/slice'; + +const persistConfig = { + key: 'token', + storage, + whitelist: ['token'], +}; + +const rootReducer = combineReducers({ + user: persistReducer(persistConfig, authSlice.reducer), +}); + +export const store = configureStore({ + reducer: rootReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), +}); + +export const persistor = persistStore(store);