diff --git a/src/pages/MyAccountPage/index.tsx b/src/pages/MyAccountPage/index.tsx
index d3cf6ca..d7b2130 100644
--- a/src/pages/MyAccountPage/index.tsx
+++ b/src/pages/MyAccountPage/index.tsx
@@ -1,16 +1,17 @@
import clsx from 'clsx';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { Toaster } from 'react-hot-toast';
import AccountDetails from '@components/MyAccount/AccountDetails';
-import Loading from '@components/UI/Loading';
+import ChangePassword from '@components/MyAccount/ChangePassword';
import { useAppDispatch, useAppSelector } from '@store/store';
import { fetchUser } from '@store/userSlice';
-import { IUser } from 'types/types';
-
export default function MyAccount() {
- const { user, status, errorMessage } = useAppSelector((state) => state.user);
+ const [isChangePassword, setIsChangePassword] = useState(false);
+
+ const { user } = useAppSelector((state) => state.user);
const dispatch = useAppDispatch();
@@ -18,47 +19,76 @@ export default function MyAccount() {
dispatch(fetchUser());
}, [dispatch]);
+ const switchToChangePasswordHandler = () => {
+ setIsChangePassword(false);
+ };
+
+ const switchToProfileHandler = () => {
+ setIsChangePassword(true);
+ };
+
return (
<>
- {status === 'pending' &&
}
- {status === 'rejected' && (
-
- {errorMessage}
-
- )}
- {status === 'fulfilled' && (
- <>
-
+
+
+ My Account
+
+
+
+ {user?.username}
+
+ {user?.email}
+
+
+
+
+
+
+
+
+ {isChangePassword ?
:
}
+
+
>
);
}
diff --git a/src/pages/ProductDetailsPage/index.tsx b/src/pages/ProductDetailsPage/index.tsx
index 0be9ac2..2f3b0f5 100644
--- a/src/pages/ProductDetailsPage/index.tsx
+++ b/src/pages/ProductDetailsPage/index.tsx
@@ -9,6 +9,7 @@ import Review from '@components/ProductDetails/Review';
import Loading from '@components/UI/Loading';
import { addToCart } from '@store/cartSlice';
+import { showModalHandler } from '@store/modalSlice';
import { fetchAllProducts } from '@store/productSlice';
import { useAppDispatch, useAppSelector } from '@store/store';
import { addWishlist, removeWishlist } from '@store/wishlistSlice';
@@ -17,7 +18,6 @@ import { INewProductToCart, IProduct, IProductData } from 'types/types';
export default function ProductDetails() {
const [quantity, setQuantity] = useState(1);
- const [isModalShow, setIsModalShow] = useState(false);
const [isOnWishList, setIsOnWishList] = useState(false);
const { productId } = useParams();
@@ -29,6 +29,7 @@ export default function ProductDetails() {
);
const { isAuthenticated } = useAppSelector((state) => state.auth);
const { wishlist } = useAppSelector((state) => state.wishlist);
+ const { isModalShow } = useAppSelector((state) => state.modal);
useEffect(() => {
if (wishlist.find((item) => item.id === product?.data.id)) {
@@ -52,17 +53,15 @@ export default function ProductDetails() {
const addToCartHandler = useCallback(() => {
const itemToCart: INewProductToCart = {
- // product_id: product?.data.id as number,
- // price: product?.data.attributes.price as string,
quantity: +quantity,
product: product?.data as IProduct,
};
dispatch(addToCart(itemToCart));
- setIsModalShow(true);
+ dispatch(showModalHandler());
}, [quantity, product?.data, dispatch]);
const closeModalBackdropHandler = () => {
- setIsModalShow(false);
+ dispatch(showModalHandler());
};
const isOnWishListHandler = () => {
diff --git a/src/pages/ProductPage/index.tsx b/src/pages/ProductPage/index.tsx
index d4c2180..348f2f5 100644
--- a/src/pages/ProductPage/index.tsx
+++ b/src/pages/ProductPage/index.tsx
@@ -18,7 +18,7 @@ export default function Product() {
<>
state.auth);
+ const { isModalShow } = useAppSelector((state) => state.modal);
const navigate = useNavigate();
const viewPasswordHandler = () => {
setIsPassView((prevState) => !prevState);
};
+ const showForgotPasswordHandler = () => {
+ dispatch(showModalHandler());
+ };
+
const loginSubmitHandler: SubmitHandler = async (
data,
event
@@ -64,83 +71,90 @@ export default function Login() {
return (
<>
-
+ {isModalShow && }
>
);
}
diff --git a/src/pages/SignUpPage/index.tsx b/src/pages/SignUpPage/index.tsx
index f1de3d6..e3c286b 100644
--- a/src/pages/SignUpPage/index.tsx
+++ b/src/pages/SignUpPage/index.tsx
@@ -59,117 +59,117 @@ export default function Register() {
return (
<>
-
>
);
diff --git a/src/store/authSlice.ts b/src/store/authSlice.ts
index b74d5d3..7668612 100644
--- a/src/store/authSlice.ts
+++ b/src/store/authSlice.ts
@@ -1,15 +1,24 @@
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AxiosResponse } from 'axios';
-import { axiosPublic } from '@utils/axiosInterceptor';
+import { axiosPrivate, axiosPublic } from '@utils/axiosInterceptor';
import { RootState } from './store';
-import { IAccount, ILogin, IRefreshToken, IRegister, IUser } from 'types/types';
+import {
+ IAccount,
+ IChangePassword,
+ IForgotPassword,
+ ILogin,
+ IRefreshToken,
+ IRegister,
+ IUser,
+} from 'types/types';
export interface IAuthState {
isAuthenticated: boolean;
accessToken: string | null;
+ refreshToken: string | null;
user: IUser | null;
status: 'pending' | 'fulfilled' | 'rejected' | 'idle';
errorMessage: string | null;
@@ -19,6 +28,7 @@ export interface IAuthState {
const initialState: IAuthState = {
isAuthenticated: false,
accessToken: null,
+ refreshToken: null,
user: null,
status: 'idle',
errorMessage: null,
@@ -54,6 +64,15 @@ export const loginUser = createAsyncThunk(
}
);
+export const logoutUser = createAsyncThunk('auth/logoutUser', async () => {
+ const response: AxiosResponse = await axiosPublic({
+ method: 'GET',
+ url: '/auth/logout',
+ withCredentials: true,
+ });
+ return response.data;
+});
+
export const refreshToken = createAsyncThunk(
'auth/refreshToken',
async (_, { getState }) => {
@@ -61,19 +80,39 @@ export const refreshToken = createAsyncThunk(
const response: AxiosResponse = await axiosPublic.post(
'/token/refresh',
{
- refreshToken: state.auth.accessToken,
+ refreshToken: state.auth.refreshToken,
}
- // {
- // withCredentials: true,
- // }
);
- const newUser: IAccount = {
- user: {
- ...(state.auth.user as IUser),
- },
- jwt: response.data?.jwt,
- };
- return newUser;
+
+ return response.data;
+ }
+);
+
+export const changePassword = createAsyncThunk(
+ 'auth/changePassword',
+ async (passwordChanged: IChangePassword, { getState }) => {
+ const state = getState() as RootState;
+ const response: AxiosResponse = await axiosPrivate.post(
+ `/auth/change-password`,
+ passwordChanged,
+ {
+ headers: {
+ Authorization: `Bearer ${state.auth.accessToken}`,
+ },
+ }
+ );
+ return response.data;
+ }
+);
+
+export const forgotPassword = createAsyncThunk(
+ 'auth/forgotPassword',
+ async (userEmail: IForgotPassword) => {
+ const response: AxiosResponse = await axiosPublic.post(
+ `/auth/forgot-password`,
+ userEmail
+ );
+ return response.data;
}
);
@@ -81,13 +120,9 @@ export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
- logoutHandler: (state) => {
- state.isAuthenticated = false;
- state.accessToken = null;
- state.user = null;
- state.errorMessage = null;
- state.successMessage = null;
- state.status = 'idle';
+ addTokenHandler: (state, action: PayloadAction) => {
+ state.accessToken = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
},
},
extraReducers: (builder) => {
@@ -101,6 +136,7 @@ export const authSlice = createSlice({
state.status = 'fulfilled';
state.isAuthenticated = true;
state.accessToken = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
state.user = action.payload.user;
state.successMessage = 'Successfully registered new user!';
})
@@ -108,10 +144,12 @@ export const authSlice = createSlice({
state.status = 'rejected';
state.isAuthenticated = false;
state.accessToken = null;
+ state.refreshToken = null;
state.user = null;
state.errorMessage = 'Failed to register new user!';
- })
+ });
+ builder
.addCase(loginUser.pending, (state) => {
state.status = 'pending';
state.errorMessage = null;
@@ -121,6 +159,7 @@ export const authSlice = createSlice({
state.status = 'fulfilled';
state.isAuthenticated = true;
state.accessToken = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
state.user = action.payload.user;
state.successMessage = 'Successfully logged in!';
})
@@ -128,20 +167,31 @@ export const authSlice = createSlice({
state.status = 'rejected';
state.isAuthenticated = false;
state.accessToken = null;
+ state.refreshToken = null;
state.user = null;
state.errorMessage = 'Failed to login user!';
- })
-
- .addCase(refreshToken.fulfilled, (state, action) => {
- state.isAuthenticated = true;
- state.accessToken = action.payload.jwt;
- state.user = action.payload.user as IAuthState['user'];
});
+
+ builder.addCase(refreshToken.fulfilled, (state, action) => {
+ state.isAuthenticated = true;
+ state.accessToken = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+ });
+
+ builder.addCase(logoutUser.fulfilled, (state) => {
+ state.isAuthenticated = false;
+ state.accessToken = null;
+ state.refreshToken = null;
+ state.user = null;
+ state.errorMessage = null;
+ state.successMessage = null;
+ state.status = 'idle';
+ });
},
});
const { actions, reducer } = authSlice;
-export const { logoutHandler } = actions;
+export const { addTokenHandler } = actions;
export default reducer;
diff --git a/src/store/modalSlice.ts b/src/store/modalSlice.ts
new file mode 100644
index 0000000..67c2c2e
--- /dev/null
+++ b/src/store/modalSlice.ts
@@ -0,0 +1,25 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+interface IModalState {
+ isModalShow: boolean;
+}
+
+const initialState: IModalState = {
+ isModalShow: false,
+};
+
+export const modalSlice = createSlice({
+ name: 'modal',
+ initialState,
+ reducers: {
+ showModalHandler: (state) => {
+ state.isModalShow = !state.isModalShow;
+ },
+ },
+});
+
+const { actions, reducer } = modalSlice;
+
+export const { showModalHandler } = actions;
+
+export default reducer;
diff --git a/src/store/store.ts b/src/store/store.ts
index 8cd882a..95b9951 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage';
import authReducer from './authSlice';
import cartReducer from './cartSlice';
+import modalReducer from './modalSlice';
import orderReducer from './orderSlice';
import productsReducer from './productSlice';
import userReducer from './userSlice';
@@ -23,6 +24,7 @@ const reducers = combineReducers({
products: productsReducer,
wishlist: wishlistReducer,
order: orderReducer,
+ modal: modalReducer,
});
const persistedReducer = persistReducer(rootPersistConfig, reducers);
diff --git a/src/store/userSlice.ts b/src/store/userSlice.ts
index 893c37b..022730a 100644
--- a/src/store/userSlice.ts
+++ b/src/store/userSlice.ts
@@ -5,7 +5,7 @@ import { axiosPrivate } from '@utils/axiosInterceptor';
import { RootState } from './store';
-import { IUser } from 'types/types';
+import { IUser, IUserUpdate } from 'types/types';
export interface IUserState {
user: IUser | null;
@@ -34,6 +34,23 @@ export const fetchUser = createAsyncThunk(
}
);
+export const updateUserDetail = createAsyncThunk(
+ 'user/updateUserDetail',
+ async (userData: IUserUpdate, { getState }) => {
+ const state = getState() as RootState;
+ const response: AxiosResponse = await axiosPrivate.put(
+ `/users/${state.user.user?.id}`,
+ userData,
+ {
+ headers: {
+ Authorization: `Bearer ${state.auth.accessToken}`,
+ },
+ }
+ );
+ return response.data;
+ }
+);
+
export const userSlice = createSlice({
name: 'user',
initialState,
@@ -55,6 +72,22 @@ export const userSlice = createSlice({
state.user = null;
state.errorMessage = 'Failed to get user!';
});
+
+ builder
+ .addCase(updateUserDetail.pending, (state) => {
+ state.status = 'pending';
+ state.errorMessage = null;
+ state.successMessage = null;
+ })
+ .addCase(updateUserDetail.fulfilled, (state, action) => {
+ state.status = 'fulfilled';
+ state.user = action.payload;
+ state.successMessage = 'User updated successfully';
+ })
+ .addCase(updateUserDetail.rejected, (state) => {
+ state.status = 'rejected';
+ state.errorMessage = 'Failed to update user detail!';
+ });
},
});
diff --git a/src/types/types.ts b/src/types/types.ts
index 315da31..a2718b1 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -29,8 +29,26 @@ export interface IUser {
phone_number: string;
}
+export interface IUserUpdate {
+ username: string;
+ email: string;
+ phone_number: string;
+ address: string;
+}
+
+export interface IChangePassword {
+ password: string;
+ currentPassword: string;
+ passwordConfirmation: string;
+}
+
+export interface IForgotPassword {
+ email: string;
+}
+
export interface IAccount {
jwt: string;
+ refreshToken: string;
user: IUser;
}
diff --git a/src/utils/formSchema.ts b/src/utils/formSchema.ts
index f7b9d05..42dad56 100644
--- a/src/utils/formSchema.ts
+++ b/src/utils/formSchema.ts
@@ -22,3 +22,26 @@ export const signInSchema = z.object({
.min(1, 'Password is required')
.min(8, 'Password must have more than 8 characters'),
});
+
+export const userDetailSchema = z.object({
+ name: z.string().min(4, 'Name is required'),
+ email: z.string().email('Invalid email').min(1, 'Email is required'),
+ phone: z.string().min(10, 'Phone number is required'),
+ address: z.string().max(300, 'Address is required'),
+});
+
+export const changePasswordSchema = z.object({
+ currentPassword: z
+ .string()
+ .min(1, 'Password is required')
+ .min(8, 'Password must have more than 8 characters'),
+ newPassword: z
+ .string()
+ .min(1, 'Password is required')
+ .min(8, 'Password must have more than 8 characters'),
+ confirmNewPassword: z.string().min(1, 'Password confirmation is required'),
+});
+
+export const forgotPasswordSchema = z.object({
+ email: z.string().email('Invalid email').min(1, 'Email is required'),
+});
diff --git a/yarn.lock b/yarn.lock
index 0331f81..a85e964 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -998,7 +998,7 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
-classnames@^2.2.6:
+classnames@^2.2.6, classnames@^2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
@@ -1092,6 +1092,11 @@ convert-source-map@^1.7.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+country-flag-icons@^1.5.4:
+ version "1.5.7"
+ resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.7.tgz#f1f2ddf14f3cbf01cba6746374aeba94db35d4b4"
+ integrity sha512-AdvXhMcmSp7nBSkpGfW4qR/luAdRUutJqya9PuwRbsBzuoknThfultbv7Ib6fWsHXC43Es/4QJ8gzQQdBNm75A==
+
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1695,6 +1700,13 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+input-format@^0.3.8:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/input-format/-/input-format-0.3.8.tgz#9445b0cab2f0457fbe36d77d607e942fd37345c5"
+ integrity sha512-tLR0XRig1xIcG1PtIpMd/uoltvkAI62CN9OIbtj4/tEJAkqTCQLNHUZ9N4M46w0dopny7Rlt/lRH5Xzp7e6F+g==
+ dependencies:
+ prop-types "^15.8.1"
+
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -1825,6 +1837,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
+libphonenumber-js@^1.10.39:
+ version "1.10.41"
+ resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.41.tgz#14b6be5894bed3385808a6a088031b5b8a27c105"
+ integrity sha512-4rmmF4u4vD3eGNuuCGjCPwRwO+fIuu1WWcS7VwbPTiMFkJd8F02v8o5pY5tlYuMR+xOvJ88mtOHpkm0Tnu2LcQ==
+
lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@@ -2237,7 +2254,7 @@ prettier@^3.0.0:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.1.tgz#65271fc9320ce4913c57747a70ce635b30beaa40"
integrity sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==
-prop-types@^15.7.2:
+prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -2305,6 +2322,17 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
+react-phone-number-input@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/react-phone-number-input/-/react-phone-number-input-3.3.2.tgz#2164e58484ddd08786636b75989f0cace75b9c12"
+ integrity sha512-h/XdyBlmy9DApKqSbmXvvvJYL1nxA3omUsCc1aeeXkt63k2QJs971jE8T/ItV9vgljBLL36BEuKs0pZlJflGew==
+ dependencies:
+ classnames "^2.3.1"
+ country-flag-icons "^1.5.4"
+ input-format "^0.3.8"
+ libphonenumber-js "^1.10.39"
+ prop-types "^15.8.1"
+
react-redux@^8.1.2:
version "8.1.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.2.tgz#9076bbc6b60f746659ad6d51cb05de9c5e1e9188"