Merge pull request #191 from bluewave-labs/feat/profile-settings-redux

Integrated redux, #173 and #182
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-06-25 17:26:06 -04:00
commit 140b8eecba
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={{
+54 -1
View File
@@ -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);
},
});
+15 -7
View File
@@ -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;
}