From 570faa40d428be5b7045be840420a109d13582d5 Mon Sep 17 00:00:00 2001 From: jaanvi Date: Tue, 29 Apr 2025 18:34:34 +0530 Subject: [PATCH] ProfilePage Edit api integration and Create EditProfile Modal --- src/components/CustomTable/customTable.tsx | 24 +- .../EditProfileModal/editProfileModal.tsx | 315 +++++++++++++ src/components/Modals/ViewModal/index.tsx | 432 ++++++++++++++++++ src/pages/ProfilePage/index.tsx | 170 ++++--- src/redux/slices/profileSlice.ts | 76 ++- 5 files changed, 935 insertions(+), 82 deletions(-) create mode 100644 src/components/Modals/EditProfileModal/editProfileModal.tsx diff --git a/src/components/CustomTable/customTable.tsx b/src/components/CustomTable/customTable.tsx index d0e86a7..9f12a9a 100644 --- a/src/components/CustomTable/customTable.tsx +++ b/src/components/CustomTable/customTable.tsx @@ -67,29 +67,27 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ backgroundColor: "#2A2A2A", cursor: "pointer", }, - // Make the "Action" header cell sticky + "&.action-cell": { right: 0, - zIndex: 11, // Higher z-index to ensure it stays above other headers + zIndex: 11, boxShadow: "-4px 0 8px rgba(0, 0, 0, 0.1)", }, }, [`&.${tableCellClasses.body}`]: { fontSize: "16px", padding: "12px 16px", - borderBottom: "1px solid rgba(255, 255, 255, 0.1)", + borderBottom: "none", color: "#333333", transition: "background-color 0.2s ease", - fontWeight:500, - // Make the "Action" body cell sticky + fontWeight: 500, "&.action-cell": { position: "sticky", right: 0, zIndex: 2, - boxShadow: "-4px 0 8px rgba(0, 0, 0, 0.1)", - backgroundColor: "#DFECF1", // Match row background + backgroundColor: "#DFECF1", "&:hover": { - backgroundColor: "#D0E1E9", // Match row hover background + backgroundColor: "#D0E1E9", }, }, }, @@ -100,11 +98,11 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ "&:hover": { backgroundColor: "#D0E1E9", }, - "& td, & th": { - borderColor: "#454545", - borderWidth: "1px", - borderBottom: "1px solid #454545", - }, + // "& td, & th": { + // borderColor: "#454545", + // borderWidth: "1px", + // // borderBottom: "1px solid #454545", + // }, })); const StyledTableContainer = styled(TableContainer)(({ theme }) => ({ diff --git a/src/components/Modals/EditProfileModal/editProfileModal.tsx b/src/components/Modals/EditProfileModal/editProfileModal.tsx new file mode 100644 index 0000000..09ab9c7 --- /dev/null +++ b/src/components/Modals/EditProfileModal/editProfileModal.tsx @@ -0,0 +1,315 @@ +import React, { useEffect, useState } from "react"; +import { Box, Button, Typography, Modal } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { useForm, Controller } from "react-hook-form"; +import { + CustomIconButton, + CustomTextField, +} from "../../AddUserModal/styled.css"; + +interface EditProfileModalProps { + open: boolean; + handleClose: () => void; + handleUpdate: ( + name: string, + phone: string, + bio?: string, + profilePhoto?: string | null + ) => void; + editUser: any; +} + +interface FormData { + name: string; + phone: string; + bio?: string; + profilePhoto?: string | null; +} + + +const EditProfileModal: React.FC = ({ + open, + handleClose, + handleUpdate, + editUser, +}) => { + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + } = useForm(); + + const [imagePreview, setImagePreview] = useState(null); + + useEffect(() => { + if (editUser) { + setValue("name", editUser.name || ""); + setValue("phone", editUser.phone || ""); + setValue("bio", editUser.bio || ""); + setImagePreview(editUser.profilePhoto || null); + } + }, [editUser, setValue]); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const imageUrl = URL.createObjectURL(file); + setImagePreview(imageUrl); + setValue("profilePhoto", imageUrl); + } + }; + + const handleModalClose = () => { + handleClose(); + + }; + + const onSubmit = (data: FormData) => { + console.log("Form Data:-----", data); + handleUpdate(data.name, data.phone, data.bio, data.profilePhoto); + reset(); + handleModalClose(); + }; + + return ( + { + if (reason !== "backdropClick") handleModalClose(); + }} + aria-labelledby="edit-profile-modal" + > + + {/* Header */} + + + Edit Profile + + + + + + + + + {/* Name Field */} + + + Full Name + + ( + + )} + /> + + + {/* Phone Field */} + + + Phone Number + + { + if (!/^[0-9]*$/.test(value)) + return "Only numbers are allowed"; + if (value.length < 6) + return "Must be at least 6 digits"; + if (value.length > 14) + return "Must be at most 14 digits"; + return true; + }, + }} + render={({ field }) => ( + + )} + /> + + + {/* Bio Field */} + + + Bio + + ( + + )} + /> + + + {/* Upload Image */} + + + Upload Profile Photo + + + {imagePreview && ( + + + Preview ( + {imagePreview.startsWith("blob") + ? "New" + : "Existing"} + ): + + Profile Preview + + )} + + + + {/* Footer */} + + + + + + + ); +}; + +export default EditProfileModal; diff --git a/src/components/Modals/ViewModal/index.tsx b/src/components/Modals/ViewModal/index.tsx index 2cff3dc..f379de5 100644 --- a/src/components/Modals/ViewModal/index.tsx +++ b/src/components/Modals/ViewModal/index.tsx @@ -169,3 +169,435 @@ export default function ViewModal({ open, setViewModal, id }: Props) { ); } + +// import { +// Box, +// Button, +// Modal, +// Typography, +// Tabs, +// Tab, +// Divider, +// Grid, +// } from "@mui/material"; +// import { AppDispatch, RootState } from "../../../redux/store/store"; +// import { useDispatch, useSelector } from "react-redux"; +// import { useEffect, useState } from "react"; +// import CloseIcon from "@mui/icons-material/Close"; +// import { managerList } from "../../../redux/slices/managerSlice"; +// import { +// AsyncThunkAction, +// ThunkDispatch, +// UnknownAction, +// } from "@reduxjs/toolkit"; + +// type Props = { +// open: boolean; +// setViewModal: Function; +// handleView: (id: string | undefined) => void; +// id?: string; +// }; + +// interface TabPanelProps { +// children?: React.ReactNode; +// index: number; +// value: number; +// } + +// const style = { +// position: "absolute", +// top: "50%", +// left: "50%", +// transform: "translate(-50%, -50%)", +// width: 600, +// borderRadius: 2, +// boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.15)", +// p: 0, +// display: "flex", +// flexDirection: "column", +// alignItems: "center", +// }; + +// function TabPanel(props: TabPanelProps) { +// const { children, value, index, ...other } = props; +// return ( +// +// ); +// } + +// export default function ViewModal({ open, setViewModal, id }: Props) { +// const { admins } = useSelector((state: RootState) => state.adminReducer); +// const { managers } = useSelector( +// (state: RootState) => state.managerReducer +// ); +// const [selectedAdmin, setSelectedAdmin] = useState(null); +// const selectedManager = managers.find( +// (manager) => String(manager.id) === String(selectedAdmin?.id) +// ); + +// const dispatch = useDispatch(); + +// const [tabValue, setTabValue] = useState(0); + +// useEffect(() => { +// if (id) { +// const admin = admins.find((admin) => admin.id === id); +// setSelectedAdmin(admin); +// } +// }, [id, admins]); +// useEffect(() => { +// if (open) { +// //dispatch(managerList()); +// } +// }, [open, dispatch]); +// const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { +// setTabValue(newValue); +// }; + +// return ( +// +// +// +// +// +// +// {selectedAdmin?.name || "Admin"}'s Details +// +// setViewModal(false)} +// sx={{ +// cursor: "pointer", +// display: "flex", +// alignItems: "center", +// color: "#D0E1E9", +// }} +// > +// +// +// +// +// + +// +// +// +// +// +// +// + +// +// {selectedAdmin ? ( +// +// +// +// Name: +// +// {selectedAdmin.name} +// +// +// +// +// +// Phone: +// +// {selectedAdmin.phone} +// +// +// +// +// +// Email: +// +// {selectedAdmin.email} +// +// +// +// +// +// Address: +// +// {selectedAdmin?.Admins?.[0] +// ?.registeredAddress ?? "N/A"} +// +// +// +// +// ) : ( +// +// No admin found with this ID +// +// )} +// + +// +// {selectedManager ? ( +// +// +// +// Manager Name: +// +// {selectedManager.name ?? "N/A"} +// +// +// +// +// +// Manager Email: +// +// {selectedManager.email ?? "N/A"} +// +// +// +// +// +// Phone: +// +// {selectedManager.phone ?? "N/A"} +// +// +// +// +// +// Station Name: +// +// {selectedManager.chargingStation +// ?.name ?? "N/A"} +// +// +// +// +// ) : ( +// +// No manager details available +// +// )} +// + +// +// {selectedAdmin?.chargingStations?.length > 0 ? ( +// +// {selectedAdmin.chargingStations.map( +// (station: any, index: number) => ( +// +// +// +// Station {index + 1}: +// +// +// Name:{" "} +// {station.name ?? "N/A"} +// +// +// Location:{" "} +// {station.location ?? "N/A"} +// +// +// Status:{" "} +// {station.status ?? "N/A"} +// +// +// {index < +// selectedAdmin.chargingStations +// .length - +// 1 && ( +// +// )} +// +// ) +// )} +// +// ) : ( +// +// No charging stations assigned +// +// )} +// + +// +// {selectedAdmin?.users?.length > 0 ? ( +// +// {selectedAdmin.users.map( +// (user: any, index: number) => ( +// +// +// +// User {index + 1}: +// +// +// Name: {user.name ?? "N/A"} +// +// +// Email: {user.email ?? "N/A"} +// +// +// Role: {user.role ?? "N/A"} +// +// +// {index < +// selectedAdmin.users.length - +// 1 && ( +// +// )} +// +// ) +// )} +// +// ) : ( +// +// No users assigned +// +// )} +// + +// +// +// +// +// +// +// ); +// } diff --git a/src/pages/ProfilePage/index.tsx b/src/pages/ProfilePage/index.tsx index b18c933..357d0f5 100644 --- a/src/pages/ProfilePage/index.tsx +++ b/src/pages/ProfilePage/index.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState, useMemo } from "react"; import { Container, Typography, @@ -14,19 +14,60 @@ import { } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch, RootState } from "../../redux/store/store"; -import { fetchAdminProfile } from "../../redux/slices/profileSlice"; +import { + fetchAdminProfile, + updateProfile, +} from "../../redux/slices/profileSlice"; +import EditIcon from "@mui/icons-material/Edit"; +import EditProfileModal from "../../components/Modals/EditProfileModal/editProfileModal"; +import { CustomIconButton } from "../../components/AddUserModal/styled.css"; const ProfilePage = () => { const dispatch = useDispatch(); - const { user, isLoading } = useSelector( - (state: RootState) => state?.profileReducer + const { user, loading } = useSelector( + (state: RootState) => state.profileReducer ); + const [openEditModal, setOpenEditModal] = useState(false); + useEffect(() => { dispatch(fetchAdminProfile()); }, [dispatch]); - if (isLoading) { + const handleOpenEditModal = () => { + setOpenEditModal(true); + }; + + const handleClose = () => { + setOpenEditModal(false); + }; + + const handleUpdate = ( + name: string, + phone: string, + bio?: string, + profilePhoto?: string | null + ) => { + console.log("Dispatching updateProfile..."); + dispatch(updateProfile({ name, phone, bio, profilePhoto })); + }; + + + // Memoizing the user data for optimization + const displayUser = useMemo( + () => ({ + name: user?.name || "N/A", + email: user?.email || "N/A", + phone: user?.phone || "N/A", + bio: user?.bio || "No bio available.", + userType: user?.userType || "N/A", + profilePhoto: user?.profilePhoto || "/avatar.png", // Default image path + }), + [user] + ); + + // Show loading indicator if data is being fetched + if (loading) { return ( { } return ( - + { p: { xs: 2, sm: 3 }, mx: "auto", backgroundColor: "#000000", - }} - // sx={{ - // width: "1132px", - // height: "331px", - // gap: "24px", - // borderRadius: "12px", - // padding: "16px", - // maxWidth: "100%", - // margin: "0 auto", - // backgroundColor: "#1C1C1C", - // }} > - - + + + + + + + + - {user?.name || "No Admin"} + {displayUser.name} - {user?.userType || "N/A"} + {displayUser.userType} + { > Personal Information - {/* Edit - */} + - + Full Name: - {user?.name || "N/A"} + {displayUser.name} Phone: - {user?.phone || "N/A"} + {displayUser.phone} Email: - {user?.email || "N/A"} + {displayUser.email} Bio: - {user?.bio || "No bio available."} + {displayUser.bio} + + ); }; diff --git a/src/redux/slices/profileSlice.ts b/src/redux/slices/profileSlice.ts index 58ef006..a2fe6ca 100644 --- a/src/redux/slices/profileSlice.ts +++ b/src/redux/slices/profileSlice.ts @@ -1,6 +1,7 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import http from "../../lib/https"; import { toast } from "sonner"; +import { string } from "prop-types"; interface User { token: string | null; @@ -9,14 +10,26 @@ interface User { email: string; userType: string; phone: string; + bio?: string; + profilePhoto?: string; } interface AuthState { user: User | null; + users: User[]; isAuthenticated: boolean; - isLoading: boolean; + loading: boolean; + error: null | string; } +const initialState: AuthState = { + users: [], + loading: false, + error: null, + isAuthenticated: false, + user: null, +}; + export const fetchAdminProfile = createAsyncThunk< User, void, @@ -32,18 +45,45 @@ export const fetchAdminProfile = createAsyncThunk< return response.data.data; } catch (error: any) { - toast.error("Error Fetching Profile" + error); + toast.error("Error Fetching Profile: " + error.message); return rejectWithValue( error?.response?.data?.message || "An error occurred" ); } }); -const initialState: AuthState = { - user: null, - isAuthenticated: false, - isLoading: false, -}; +export const updateProfile = createAsyncThunk< + User, + { + name: string; + phone?: string; + bio?: string; + profilePhoto?: string | null; + }, + { rejectValue: string } +>( + "updateProfile", + async ({ name, phone, bio, profilePhoto }, { rejectWithValue }) => { + try { + const payload: any = { name }; + if (phone) payload.phone = phone; + if (bio) payload.bio = bio; + if (profilePhoto) payload.profilePhoto = profilePhoto; + + const response = await http.put("/edit-profile", payload); + console.log("-----------", response); + + toast.success("Profile updated successfully"); + return response.data.data; + } catch (error: any) { + toast.error("Error updating the profile: " + error.message); + return rejectWithValue( + error?.response?.data?.message || "An error occurred" + ); + } + } +); + const profileSlice = createSlice({ name: "profile", @@ -52,15 +92,29 @@ const profileSlice = createSlice({ extraReducers: (builder) => { builder .addCase(fetchAdminProfile.pending, (state) => { - state.isLoading = true; + state.loading = true; }) .addCase(fetchAdminProfile.fulfilled, (state, action) => { - state.isLoading = false; + state.loading = false; state.user = action.payload; state.isAuthenticated = true; }) - .addCase(fetchAdminProfile.rejected, (state) => { - state.isLoading = false; + .addCase(fetchAdminProfile.rejected, (state, action) => { + state.loading = false; + state.error = action.payload; + }) + .addCase(updateProfile.fulfilled, (state, action) => { + state.loading = false; + state.user = action.payload; // Update current user only + toast.success("Profile Details updated successfully"); + }) + + .addCase(updateProfile.rejected, (state, action) => { + state.loading = false; + state.error = action.payload; + }) + .addCase(updateProfile.pending, (state) => { + state.loading = true; }); }, });