Merge pull request #191 from bluewave-labs/feat/profile-settings-redux
Integrated redux, #173 and #182
This commit is contained in:
5 files changed
+124
-42
No files matched your search
@@ -8,6 +8,8 @@ import EmailTextField from "../../TextFields/Email/EmailTextField";
|
||||
import StringTextField from "../../TextFields/Text/TextField";
|
||||
import Avatar from "../../Avatar";
|
||||
import { editProfileValidation } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -19,22 +21,26 @@ import { editProfileValidation } from "../../../Validation/validation";
|
||||
|
||||
const ProfilePanel = () => {
|
||||
const theme = useTheme();
|
||||
//TODO - use redux loading state
|
||||
//!! - currently all loading buttons are tied to the same state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
//for testing, will tweak when I implement redux slice
|
||||
const { user, authToken, isLoading } = useSelector((state) => state.auth);
|
||||
const idToName = {
|
||||
"edit-first-name": "firstname",
|
||||
"edit-last-name": "lastname",
|
||||
"edit-email": "email",
|
||||
//Disabled for now, will revisit in the future
|
||||
// "edit-email": "email",
|
||||
};
|
||||
const [localData, setLocalData] = useState({
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
email: "",
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
//Disabled for now, will revisit in the future
|
||||
// email: user.email,
|
||||
//TODO - upload picture
|
||||
profilePicUrl: "placeholder",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loadingPfp, setLoadingPfp] = useState(false);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
@@ -43,7 +49,7 @@ const ProfilePanel = () => {
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
|
||||
const validation = editProfileValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
@@ -63,29 +69,31 @@ const ProfilePanel = () => {
|
||||
|
||||
//TODO - implement delete profile picture function
|
||||
const handleDeletePicture = () => {
|
||||
setIsLoading(true);
|
||||
setLoadingPfp(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setLoadingPfp(false);
|
||||
}, 2000);
|
||||
};
|
||||
//TODO - implement update profile function
|
||||
const handleUpdatePicture = () => {};
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
//TODO - implement delete account function
|
||||
const handleDeleteAccount = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
const handleUpdatePicture = () => {
|
||||
setLocalData((prev) => ({ ...prev, profilePicUrl: file }));
|
||||
console.log(localData);
|
||||
};
|
||||
//TODO - implement delete account function
|
||||
const handleDeleteAccount = () => {};
|
||||
//TODO - implement save profile function
|
||||
const handleSaveProfile = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
const handleSaveProfile = (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstname === user.firstname &&
|
||||
localData.lastname === user.lastname &&
|
||||
localData.profilePicUrl === user.profilePicUrl
|
||||
) {
|
||||
//TODO - add toast(profile data is unchanged) and maybe disable button
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(update({ authToken, localData }));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,6 +109,7 @@ const ProfilePanel = () => {
|
||||
<StringTextField
|
||||
id="edit-first-name"
|
||||
label={null}
|
||||
value={localData.firstname}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
@@ -125,6 +134,7 @@ const ProfilePanel = () => {
|
||||
<StringTextField
|
||||
id="edit-last-name"
|
||||
label={null}
|
||||
value={localData.lastname}
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
onChange={handleChange}
|
||||
@@ -145,17 +155,19 @@ const ProfilePanel = () => {
|
||||
Email
|
||||
</Typography>
|
||||
<Typography variant="h5" component="p">
|
||||
After updating, you'll receive a confirmation email.
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<EmailTextField
|
||||
id="edit-email"
|
||||
label={null}
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-email"]] ? true : false}
|
||||
// onChange={handleChange}
|
||||
// error={errors[idToName["edit-email"]] ? true : false}
|
||||
disabled={true}
|
||||
/>
|
||||
{errors[idToName["edit-email"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
@@ -179,8 +191,8 @@ const ProfilePanel = () => {
|
||||
{/* TODO - Update placeholder values with redux data */}
|
||||
<Avatar
|
||||
src="/static/images/avatar/2.jpg"
|
||||
firstName="Jackie"
|
||||
lastName="Dawn"
|
||||
firstName={localData.firstname}
|
||||
lastName={localData.lastname}
|
||||
sx={{
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
@@ -192,7 +204,7 @@ const ProfilePanel = () => {
|
||||
level="tertiary"
|
||||
label="Delete"
|
||||
onClick={handleDeletePicture}
|
||||
isLoading={isLoading}
|
||||
isLoading={loadingPfp}
|
||||
/>
|
||||
{/* TODO - modal popup for update pfp? */}
|
||||
<Button
|
||||
|
||||
@@ -13,12 +13,14 @@ import { useTheme } from "@mui/material";
|
||||
* @param {function} props.onChange - The function to call when the text field changes.
|
||||
* @param {string} props.id - The id of the text field.
|
||||
* @param {string} [props.label="Email"] - The label of the text field.
|
||||
* @param {string} props.value - The value of the text field.
|
||||
* @param {string} props.variant - The variant of the text field.
|
||||
* @param {string} props.placeholder - The placeholder of the text field.
|
||||
* @param {React.Element} props.icon - The icon to be displayed in the text field.
|
||||
* @param {string} props.helperText - The helper text to be displayed below the text field.
|
||||
* @param {boolean} props.error - If true, the text field will indicate an error state.
|
||||
*
|
||||
* @param {boolean} props.disabled - If true, the text field will be disabled and cannot be interacted with.
|
||||
*
|
||||
* @example
|
||||
* // To use this component, import it and use it in your JSX like this:
|
||||
* <EmailTextField id="email" variant="outlined" placeholder="Enter your email" />
|
||||
@@ -30,12 +32,14 @@ const EmailTextField = ({
|
||||
onChange,
|
||||
id,
|
||||
label = "Email",
|
||||
value = undefined,
|
||||
variant,
|
||||
placeholder,
|
||||
autoComplete,
|
||||
icon,
|
||||
helperText,
|
||||
error,
|
||||
disabled
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -59,9 +63,11 @@ const EmailTextField = ({
|
||||
error={error}
|
||||
className="email-text-field-input"
|
||||
id={id}
|
||||
value={value}
|
||||
variant={variant}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
disabled={disabled}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -78,7 +84,7 @@ const EmailTextField = ({
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{!error && showIcon && (
|
||||
{!error && showIcon && !disabled && (
|
||||
<HelpOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ width: "16px", height: "16px" }}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTheme } from "@mui/material";
|
||||
* @param {function} props.onChange - The function to call when the text field changes.
|
||||
* @param {string} props.id - The ID of the text field.
|
||||
* @param {string} props.label - The label text for the text field.
|
||||
* @param {string} props.value - The value of the text field.
|
||||
* @param {string} props.variant - The variant of the text field.
|
||||
* @param {string} props.placeholder - The placeholder text for the text field.
|
||||
* @param {ReactNode} props.icon - The icon to display in the text field.
|
||||
@@ -22,6 +23,7 @@ const StringTextField = ({
|
||||
autoComplete,
|
||||
id,
|
||||
label,
|
||||
value = undefined,
|
||||
variant,
|
||||
placeholder,
|
||||
icon,
|
||||
@@ -51,6 +53,7 @@ const StringTextField = ({
|
||||
className="email-text-field-input"
|
||||
id={id}
|
||||
variant={variant}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
InputProps={{
|
||||
|
||||
@@ -38,6 +38,40 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const update = createAsyncThunk(
|
||||
"auth/update",
|
||||
async (data, thunkApi) => {
|
||||
const { authToken: token, localData: form } = data;
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
//1.5s delay to show loading spinner
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const res = await axios.post(`${BASE_URL}/auth/user/${user._id}`, form, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAccount = createAsyncThunk(
|
||||
"auth/delete",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
//TODO
|
||||
} catch (error) {}
|
||||
}
|
||||
);
|
||||
|
||||
const handleAuthFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
@@ -54,6 +88,19 @@ const handleAuthRejected = (state, action) => {
|
||||
? action.payload.msg
|
||||
: "Failed to login or register";
|
||||
};
|
||||
const handleUpdateFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.user = action.payload.data;
|
||||
};
|
||||
const handleUpdateRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update profile data.";
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
@@ -80,7 +127,13 @@ const authSlice = createSlice({
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(login.fulfilled, handleAuthFulfilled)
|
||||
.addCase(login.rejected, handleAuthRejected);
|
||||
.addCase(login.rejected, handleAuthRejected)
|
||||
// Update thunk
|
||||
.addCase(update.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(update.fulfilled, handleUpdateFulfilled)
|
||||
.addCase(update.rejected, handleUpdateRejected);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
.announcement-tile {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
[class$="-form"] {
|
||||
.settings [class$="-form"] {
|
||||
width: inherit;
|
||||
}
|
||||
[class$="__wrapper"],
|
||||
[class$="__wrapper compact"] {
|
||||
.settings [class$="__wrapper"],
|
||||
.settings [class$="__wrapper compact"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@@ -171,11 +171,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings .MuiStack-root:has(p.MuiTypography-root.input-error){
|
||||
.settings .MuiStack-root:has(p.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settings p.MuiTypography-root.input-error{
|
||||
.settings .text-field-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.settings p.MuiTypography-root.input-error {
|
||||
color: var(--env-var-color-24);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
@@ -187,10 +195,10 @@
|
||||
}
|
||||
|
||||
.edit-profile-form__wrapper .MuiFormControl-root,
|
||||
.edit-password-form__wrapper .MuiFormControl-root{
|
||||
.edit-password-form__wrapper .MuiFormControl-root {
|
||||
width: 100%;
|
||||
}
|
||||
.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root),
|
||||
.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root){
|
||||
.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root) {
|
||||
margin: 0;
|
||||
}
|
||||
Reference in new issue
Block a user