diff --git a/src/Components/Common/Sidebar/SidebarUserCard.tsx b/src/Components/Common/Sidebar/SidebarUserCard.tsx index 02170f453d..9153935e16 100644 --- a/src/Components/Common/Sidebar/SidebarUserCard.tsx +++ b/src/Components/Common/Sidebar/SidebarUserCard.tsx @@ -15,8 +15,14 @@ const SidebarUserCard = ({ shrinked }: { shrinked: boolean }) => { shrinked ? "mx-auto flex-col" : "mx-5" } transition-all duration-200 ease-in-out`} > - - + + profile
void) | undefined; + onSave?: (() => void) | undefined; + onDelete?: (() => void) | undefined; + user: UserModel; +} + +const ProfilePicUploadModal = ({ + open, + onClose, + onSave, + onDelete, + user, +}: Props) => { + const [isUploading, setIsUploading] = useState(false); + const [selectedFile, setSelectedFile] = useState(); + const [preview, setPreview] = useState(); + const [isCameraOpen, setIsCameraOpen] = useState(false); + const webRef = useRef(null); + const [previewImage, setPreviewImage] = useState(null); + const [isCaptureImgBeingUploaded, setIsCaptureImgBeingUploaded] = + useState(false); + const FACING_MODE_USER = "user"; + const FACING_MODE_ENVIRONMENT = { exact: "environment" }; + const [facingMode, setFacingMode] = useState(FACING_MODE_USER); + const videoConstraints = { + width: 1280, + height: 720, + facingMode: "user", + }; + const { width } = useWindowDimensions(); + const LaptopScreenBreakpoint = 640; + const isLaptopScreen = width >= LaptopScreenBreakpoint; + const { t } = useTranslation(); + const handleSwitchCamera = useCallback(() => { + setFacingMode((prevState: any) => + prevState === FACING_MODE_USER + ? FACING_MODE_ENVIRONMENT + : FACING_MODE_USER, + ); + }, []); + + const captureImage = () => { + setPreviewImage(webRef.current.getScreenshot()); + const canvas = webRef.current.getCanvas(); + canvas?.toBlob((blob: Blob) => { + const myFile = new File([blob], "image.png", { + type: blob.type, + }); + setSelectedFile(myFile); + }); + }; + const closeModal = () => { + setPreview(undefined); + setSelectedFile(undefined); + onClose?.(); + }; + + useEffect(() => { + if (selectedFile) { + const objectUrl = URL.createObjectURL(selectedFile); + setPreview(objectUrl); + return () => URL.revokeObjectURL(objectUrl); + } + }, [selectedFile]); + + const onSelectFile: ChangeEventHandler = (e) => { + if (!e.target.files || e.target.files.length === 0) { + setSelectedFile(undefined); + return; + } + setSelectedFile(e.target.files[0]); + }; + + const handleUpload = async () => { + setIsCaptureImgBeingUploaded(true); + if (!selectedFile) { + setIsCaptureImgBeingUploaded(false); + closeModal(); + return; + } + + const formData = new FormData(); + formData.append("profile_picture_url", selectedFile); + const url = `/api/v1/users/${user.username}/profile_picture/`; + setIsUploading(true); + + uploadFile( + url, + formData, + "POST", + { + Authorization: + "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), + }, + (xhr: XMLHttpRequest) => { + if (xhr.status === 200) { + Success({ msg: "Profile Picture updated." }); + } else { + Notification.Error({ + msg: "Something went wrong!", + }); + setIsUploading(false); + } + }, + null, + () => { + Notification.Error({ + msg: "Network Failure. Please check your internet connectivity.", + }); + setIsUploading(false); + }, + ); + + await sleep(1000); + setIsUploading(false); + setIsCaptureImgBeingUploaded(false); + onSave && onSave(); + closeModal(); + }; + + const handleDelete = async () => { + const { res } = await request(routes.deleteProfilePicture, { + pathParams: { username: user.username }, + }); + if (res?.ok) { + Success({ msg: "Profile picture deleted" }); + onDelete?.(); + closeModal(); + } + }; + + const hasImage = !!(preview || user.read_profile_picture_url); + const imgSrc = + preview || `${user.read_profile_picture_url}?requested_on=${Date.now()}`; + + const dragProps = useDragAndDrop(); + const onDrop = (e: React.DragEvent) => { + e.preventDefault(); + dragProps.setDragOver(false); + const dropedFile = e?.dataTransfer?.files[0]; + if (dropedFile.type.split("/")[0] !== "image") + return dragProps.setFileDropError("Please drop an image file to upload!"); + setSelectedFile(dropedFile); + }; + const commonHint = ( + <> + {t("max_size_for_image_uploaded_should_be")} 1mb. +
+ {t("allowed_formats_are")} jpg,png,jpeg. + {t("recommended_aspect_ratio_for")} user profile image is 1:1 + + ); + + return ( + +
+ {!isCameraOpen ? ( +
+ {hasImage ? ( + <> +
+ profile-pic +
+ + ) : ( +
+ +

+ {dragProps.fileDropError !== "" + ? dragProps.fileDropError + : `${t("drag_drop_image_to_upload")}`} +

+

+ No Profile image uploaded yet. {commonHint} +

+
+ )} + +
+
+ +
+
+ { + setIsCameraOpen(true); + }} + > + {`${t("open")} ${t("camera")}`} + + { + e.stopPropagation(); + closeModal(); + dragProps.setFileDropError(""); + }} + disabled={isUploading} + /> + {user.read_profile_picture_url && ( + + {t("delete")} + + )} + + {isUploading ? ( + + ) : ( + + )} + + {isUploading ? `${t("uploading")}...` : `${t("save")}`} + + +
+ + ) : ( +
+
+ {!previewImage ? ( + <> + + + ) : ( + <> + + + )} +
+ {/* buttons for mobile screens */} +
+
+ {!previewImage ? ( + + {t("switch")} + + ) : ( + <> + )} +
+
+ {!previewImage ? ( + <> +
+ { + captureImage(); + }} + className="my-2 w-full" + > + {t("capture")} + +
+ + ) : ( + <> +
+ { + setPreviewImage(null); + }} + className="my-2 w-full" + disabled={isUploading} + > + {t("retake")} + + + {isCaptureImgBeingUploaded && ( + + )} + {t("submit")} + +
+ + )} +
+
+ { + setPreviewImage(null); + setIsCameraOpen(false); + webRef.current.stopCamera(); + }} + className="border-grey-200 my-2 w-full border-2" + > + {t("close")} + +
+
+ {/* buttons for laptop screens */} + + )} +
+ + ); +}; + +export default ProfilePicUploadModal; diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 50dbc5890c..e0dfb2c247 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -16,7 +16,12 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import { GenderType, SkillModel, UpdatePasswordForm } from "../Users/models"; +import { + GenderType, + SkillModel, + UpdatePasswordForm, + UserModel, +} from "../Users/models"; import UpdatableApp, { checkForUpdate } from "../Common/UpdatableApp"; import dayjs from "../../Utils/dayjs"; import useAuthUser, { useAuthContext } from "../../Common/hooks/useAuthUser"; @@ -26,6 +31,7 @@ import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import DateFormField from "../Form/FormFields/DateFormField"; import { validateRule } from "./UserAdd"; +import ProfilePicUploadModal from "./ProfilePicUploadModal"; const Loading = lazy(() => import("../Common/Loading")); type EditForm = { @@ -112,6 +118,8 @@ const editFormReducer = (state: State, action: Action) => { export default function UserProfile() { const { signOut } = useAuthContext(); const [states, dispatch] = useReducer(editFormReducer, initialState); + const [editProfilePic, setEditProfilePic] = useState(false); + const [imageKey, setImageKey] = useState(Date.now()); const [updateStatus, setUpdateStatus] = useState({ isChecking: false, isUpdateAvailable: false, @@ -452,6 +460,17 @@ export default function UserProfile() { }; return (
+ + userData?.read_profile_picture_url + ? setImageKey(Date.now()) + : refetchUserData() + } + onClose={() => setEditProfilePic(false)} + onDelete={() => refetchUserData()} + user={userData ?? ({} as UserModel)} + />
@@ -462,6 +481,34 @@ export default function UserProfile() {

Local Body, District and State are Non Editable Settings.

+
+
setEditProfilePic(!editProfilePic)} + > + +
+ + {`${userData?.read_profile_picture_url ? "Edit" : "Upload"}`} +
+
+
+

+ {userData?.first_name} {userData?.last_name} +

+

+ @{userData?.username} +

+
+
setShowEdit(!showEdit)} diff --git a/src/Components/Users/models.tsx b/src/Components/Users/models.tsx index 1bbe494b9e..3dbb637e11 100644 --- a/src/Components/Users/models.tsx +++ b/src/Components/Users/models.tsx @@ -32,6 +32,7 @@ export type UserModel = UserBareMinimum & { phone_number?: string; alt_phone_number?: string; gender?: GenderType; + read_profile_picture_url?: string; date_of_birth: Date | null | string; is_superuser?: boolean; verified?: boolean; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index e60df9978d..44b32c6a9b 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -302,6 +302,19 @@ const routes = { TBody: Type>(), }, + updateProfilePicture: { + path: "/api/v1/users/{username}/profile_picture/", + method: "PATCH", + TRes: Type(), + TBody: Type<{ profile_picture_url: string }>(), + }, + + deleteProfilePicture: { + path: "/api/v1/users/{username}/profile_picture/", + method: "DELETE", + TRes: Type(), + }, + deleteUser: { path: "/api/v1/users/{username}/", method: "DELETE",