Skip to content

Commit

Permalink
feat(users profile): users should view and update his/her profile
Browse files Browse the repository at this point in the history
-Ensures that user can view his or her profile
-Ensure that user can update his or her profile

[Deliver #187419128]
  • Loading branch information
niyobertin committed Jun 27, 2024
1 parent 7136e3b commit e0f3dfb
Show file tree
Hide file tree
Showing 17 changed files with 3,137 additions and 2 deletions.
2,472 changes: 2,472 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "^3.6.0",
"@mui/material": "^5.15.20",
"@reduxjs/toolkit": "^2.2.5",
Expand All @@ -28,10 +32,12 @@
"axios-mock-adapter": "^1.22.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"install": "^0.13.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^3.0.7",
"prop-types": "^15.8.1",
"npm": "^10.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
Expand Down
Binary file added public/Screenshot 2024-06-04 at 14.38.58.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/components/profile/getProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

interface ProfileDataProps {
label: string;
value: string | number | null;
}

const DisplayProfileData: React.FC<ProfileDataProps> = ({ label, value }) => (
<center>
<div className="block sm:flex justify-between border-solid border-b-[1px] border-gray-200 pt-[1%]">
<p className="flex w-[100%] text-md sm:text-lg">{label}</p>
<p className="flex justify-start w-[100%] text-sm sm:text-lg font-light">
{value}
</p>
</div>
</center>
);

export default DisplayProfileData;
11 changes: 11 additions & 0 deletions src/components/profile/linkToUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from "react-router-dom";

interface PropTypes {
link: string;
children: React.ReactNode;
}
const LinkToUpdatePage = ({ link, children }: PropTypes) => (
<Link to={link}>{children}</Link>
);

export default LinkToUpdatePage;
10 changes: 10 additions & 0 deletions src/components/profile/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const Spinner = () => (
<div className="flex justify-center items-center mt-[80%] sm:mt-[25%]">
<div
className="inline-block h-8 w-8 sm:h-16 sm:w-16 animate-spin rounded-full border-4 border-solid border-[#DB4444] border-e-transparent text-danger motion-reduce:animate-[spin_1.5s_linear_infinite]"
role="status"
/>
</div>
);

export default Spinner;
25 changes: 25 additions & 0 deletions src/components/profile/updateProfileButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";

interface ButtonProps {
text: string;
disabled?: boolean;
dataTestId?: string;
backgroundColor?: string;
}

const Button: React.FC<ButtonProps> = ({
text,
disabled,
dataTestId,
backgroundColor,
}) => (
<button
type="submit"
className={`${backgroundColor} text-white p-1 sm:py-3 px-2 sm:px-12 my-4 text-normal md:text-lg rounded-sm cursor-pointer`}
disabled={disabled}
data-testid={dataTestId}
>
{text}
</button>
);
export default Button;
39 changes: 39 additions & 0 deletions src/components/profile/updateProfileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from "react";

type TextInputProps = {
label: string;
defaultValue?: string | number;
register: any;
name: string;
error?: any;
type?: string;
};

const ProfileTextInput: React.FC<TextInputProps> = ({
label,
defaultValue,
register,
name,
error,
type = "text",
}) => (
<div>
<label
className="text-md sm:text-lg block mb-1 sm:mb-2 text-dark-gray"
htmlFor={name}
>
{label}
</label>
<input
defaultValue={defaultValue}
type={type}
id={name}
className="w-full bg-gray-100 text-[#161616] border-[0.5px] border-[#E5E5E5] py-1 sm:py-2 text-md font-light rounded-[8px] px-4 focus:outline-none"
{...register(name)}
/>
{error && <p className="text-red-500">{error}</p>}
</div>
);

export default ProfileTextInput;
263 changes: 263 additions & 0 deletions src/pages/updateProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import React, { useEffect, useState } from "react";
import { BiImageAdd } from "react-icons/bi";
import { yupResolver } from "@hookform/resolvers/yup";
import { SubmitHandler, useForm } from "react-hook-form";
import { useSelector } from "react-redux";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";

import LinkPages from "../components/common/auth/LinkPages";
import { RootState } from "../redux/store";
import profileSchema from "../schemas/profileSchema";
import ProfileTextInput from "../components/profile/updateProfileInput";
import { getProfile } from "../redux/reducers/profileSlice";
import { updateProfile } from "../redux/reducers/updateProfileSlice";
import { useAppDispatch } from "../redux/hooks";
import sideImage from "../assets/sideImage.png";
import Button from "../components/profile/updateProfileButtons";

const UpdateUserProfile: React.FC = () => {
const dispatch = useAppDispatch();
const { profile } = useSelector((state: RootState) => state.usersProfile);
useEffect(() => {
// @ts-ignore
dispatch(getProfile());
}, [dispatch]);
// @ts-ignore
const [imagePreview, setImagePreview] = useState<string>(
// @ts-ignore
profile?.profileImage,
);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const { loading } = useSelector(
(state: RootState) => state.updateUsersProfile,
);

const {
register,
handleSubmit,
formState: { errors },
setValue,
} = useForm({
resolver: yupResolver(profileSchema),
});

useEffect(() => {
if (profile) {
setValue("fullName", profile.fullName || "");
setValue("gender", profile.gender || "");
setValue("birthdate", profile.birthdate || "");
setValue("preferredLanguage", profile.preferredLanguage || "");
setValue("preferredCurrency", profile.preferredCurrency || "");
setValue("street", profile.street || "");
setValue("city", profile.city || "");
setValue("state", profile.state || "");
setValue("postalCode", profile.postalCode || "");
setValue("country", profile.country || "");
setImagePreview(profile.profileImage);
}
}, [profile, setValue]);
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
// @ts-ignore
setSelectedImage(file);
}
};
const convertUrlToFile = async (url: string): Promise<File> => {
const response = await fetch(url);
const blob = await response.blob();
const filename = url.split("/").pop() || "profileImage";
return new File([blob], filename, { type: blob.type });
};

interface UpdateProfileProps {
fullName?: string;
gender?: string;
birthdate?: string;
preferredLanguage?: string;
preferredCurrency?: string;
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
profileImage?: File | null;
}

const onSubmit: SubmitHandler<UpdateProfileProps> = async (data) => {
const formData = new FormData();
if (selectedImage) {
formData.append("profileImage", selectedImage);
} else {
// @ts-ignore
const newFile = await convertUrlToFile(profile?.profileImage);
formData.append("profileImage", newFile);
}
formData.append("fullName", data.fullName || "");
formData.append("gender", data.gender || "");
formData.append("birthdate", data.birthdate || "");
formData.append("preferredLanguage", data.preferredLanguage || "");
formData.append("preferredCurrency", data.preferredCurrency || "");
formData.append("street", data.street || "");
formData.append("city", data.city || "");
formData.append("state", data.state || "");
formData.append("postalCode", data.postalCode || "");
formData.append("country", data.country || "");
try {
const result = await dispatch(updateProfile(formData)).unwrap();
toast.success("Profile updated successfully!");
console.log(result);
} catch (error) {
if (error instanceof AxiosError) {
toast.error(`Error updating profile: ${error.message}`);
}
}
};

return (
<div className="w-full overflow-y-hidden">
<ToastContainer />
<p className="flex justify-end mr-[5%] gap-2">
Welcome
<span className="text-[#DB4444]">
{' '}
{profile?.fullName}
</span>
</p>

<div className="flex ">
<div className="hidden lg:flex w-[100%] h-[30rem] w-[96%] ml-[5%] ">
<div className="hidden min-h-screen lg:flex w-[100%] xl:w-[90%]">
<img className="w-full h-[41rem]" src={sideImage} alt="Profile" />
</div>
</div>
<div className="w-[100%] sm:w-[96%] mr-[5%] ml-[4%] sm:ml-[0%] sm:mr-[5%]">
<h1 className="text-[#DB4444] text-lg font-bold">
{" "}
Update your profile
</h1>
{/* @ts-ignore */}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 py-0 md:grid-cols-2 lg:grid-cols-2 w-[100%] gap-4">
<p className="text-lg block mb-2 text-dark-gray flex items-center">
Profile Image
</p>
<div className="flex flex-col items-center">
<label
htmlFor="profileImage"
className=" w-20 h-20 mt-2 flex items-center justify-center cursor-pointer relative"
>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="w-full h-full opacity-0 absolute top-0 left-0 cursor-pointer "
/>
<BiImageAdd className="absolute inset-14 bg-[#DB4444] h-[30%] w-[30%] text-white rounded-[50%]" />

{imagePreview ? (
<img
src={imagePreview}
alt="Profile Preview"
className="w-full h-full object-cover rounded-[50%]"
/>
) : (
<span>Upload Image</span>
)}
</label>
</div>

<ProfileTextInput
label="Full name"
defaultValue={profile?.fullName}
register={register}
name="fullName"
error={errors.fullName?.message}
/>
<ProfileTextInput
label="Gender"
defaultValue={profile?.gender}
register={register}
name="gender"
error={errors.gender?.message}
/>
<ProfileTextInput
label="Birth date"
defaultValue={profile?.birthdate}
register={register}
name="birthdate"
error={errors.birthdate?.message}
/>
<ProfileTextInput
label="Preferred language"
defaultValue={profile?.preferredLanguage}
register={register}
name="preferredLanguage"
error={errors.preferredLanguage?.message}
/>
<ProfileTextInput
label="Preferred currency"
defaultValue={profile?.preferredCurrency}
register={register}
name="preferredCurrency"
error={errors.preferredCurrency?.message}
/>
<ProfileTextInput
label="Street"
defaultValue={profile?.street}
register={register}
name="street"
error={errors.state?.message}
/>
<ProfileTextInput
label="City"
defaultValue={profile?.city}
register={register}
name="city"
error={errors.city?.message}
/>
<ProfileTextInput
label="Postal Code"
defaultValue={profile?.postalCode}
register={register}
name="postalCode"
error={errors.postalCode?.message}
/>
<ProfileTextInput
label="State"
defaultValue={profile?.state}
register={register}
name="state"
error={errors.state?.message}
/>
<ProfileTextInput
label="Country"
defaultValue={profile?.country}
register={register}
name="country"
error={errors.country?.message}
/>
</div>
<div className="flex justify-end items-center gap-4 text-none">
<LinkPages description="" link="/profile" text="Cancel" />
<Button
text={loading ? "Loading..." : "Save changes"}
backgroundColor="bg-[#DB4444]"
disabled={loading}
data-testid="updating-btn"
/>
</div>
</form>
</div>
</div>
</div>
);
};

export default UpdateUserProfile;
Loading

0 comments on commit e0f3dfb

Please sign in to comment.