diff --git a/src/Common/hooks/useAuthUser.ts b/src/Common/hooks/useAuthUser.ts index 78bf93fa8f8..ae181f62ca8 100644 --- a/src/Common/hooks/useAuthUser.ts +++ b/src/Common/hooks/useAuthUser.ts @@ -9,6 +9,7 @@ type AuthContextType = { user: UserModel | undefined; signIn: (creds: LoginCredentials) => Promise; signOut: () => Promise; + refetchUser: () => Promise>; }; export const AuthUserContext = createContext(null); diff --git a/src/Components/Common/Sidebar/SidebarUserCard.tsx b/src/Components/Common/Sidebar/SidebarUserCard.tsx index 69678f676d4..f8078f405c8 100644 --- a/src/Components/Common/Sidebar/SidebarUserCard.tsx +++ b/src/Components/Common/Sidebar/SidebarUserCard.tsx @@ -1,7 +1,7 @@ import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import CareIcon from "../../../CAREUI/icons/CareIcon"; -import { formatName } from "../../../Utils/utils"; +import { classNames, formatName } from "../../../Utils/utils"; import useAuthUser, { useAuthContext } from "../../../Common/hooks/useAuthUser"; const SidebarUserCard = ({ shrinked }: { shrinked: boolean }) => { @@ -15,8 +15,15 @@ const SidebarUserCard = ({ shrinked }: { shrinked: boolean }) => { shrinked ? "mx-auto flex-col" : "mx-5" } transition-all duration-200 ease-in-out`} > - - + + profile
void; + onSave?: () => void; + onDelete?: () => void; + onRefetch?: () => void; +} + +const ProfilePicUploadModal = ({ + open, + onClose, + onSave, + onDelete, + onRefetch, +}: Props) => { + const user = useAuthUser(); + 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 FACING_MODE_USER = "user"; + const FACING_MODE_ENVIRONMENT = { exact: "environment" }; + const [uploadPercent, setUploadPercent] = useState(0); + + const [facingMode, setFacingMode] = useState<"front" | "rear">("front"); + const videoConstraints = { + width: 1280, + height: 720, + facingMode: + facingMode === "front" ? FACING_MODE_USER : FACING_MODE_ENVIRONMENT, + }; + const { width } = useWindowDimensions(); + const LaptopScreenBreakpoint = 640; + const isLaptopScreen = width >= LaptopScreenBreakpoint; + const { t } = useTranslation(); + const handleSwitchCamera = useCallback(() => { + setFacingMode((prev) => (prev === "front" ? "rear" : "front")); + }, []); + + const captureImage = () => { + if (!webRef.current) return; + setPreviewImage(webRef.current.getScreenshot()); + const canvas = webRef.current.getCanvas(); + canvas?.toBlob((blob: Blob | null) => { + if (!blob) return; + 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]); + }; + + useEffect(() => { + if (uploadPercent === 100) { + setIsUploading(false); + onSave?.(); + closeModal(); + setUploadPercent(0); + onRefetch?.(); + } + }, [uploadPercent]); + + const handleUpload = async () => { + setIsUploading(true); + if (!selectedFile) { + setIsUploading(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); + } + }, + setUploadPercent, + () => { + Notification.Error({ + msg: "Network Failure. Please check your internet connectivity.", + }); + setIsUploading(false); + }, + ); + }; + + 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")} + + + {isUploading && ( + + )} + {t("submit")} + +
+ + )} +
+
+ { + setPreviewImage(null); + setIsCameraOpen(false); + }} + className="border-grey-200 my-2 w-full border-2" + > + {t("close")} + +
+
+ {/* buttons for laptop screens */} +
+
+ + + {`${t("switch")} ${t("camera")}`} + +
+ +
+
+ {!previewImage ? ( + <> +
+ { + captureImage(); + }} + > + + {t("capture")} + +
+ + ) : ( + <> +
+ { + setPreviewImage(null); + }} + > + {t("retake")} + + + {isUploading ? ( + <> + + {`${t("submitting")}...`} + + ) : ( + <> {t("submit")} + )} + +
+ + )} +
+
+ { + setPreviewImage(null); + setIsCameraOpen(false); + }} + > + {`${t("close")} ${t("camera")}`} + +
+
+
+ )} +
+ + ); +}; + +export default ProfilePicUploadModal; diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 21a2e077442..463ed66fc8b 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -26,6 +26,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"; import { useTranslation } from "react-i18next"; const Loading = lazy(() => import("../Common/Loading")); @@ -112,8 +113,9 @@ const editFormReducer = (state: State, action: Action) => { export default function UserProfile() { const { t } = useTranslation(); - const { signOut } = useAuthContext(); + const { signOut, refetchUser } = useAuthContext(); const [states, dispatch] = useReducer(editFormReducer, initialState); + const [editProfilePic, setEditProfilePic] = useState(false); const [updateStatus, setUpdateStatus] = useState({ isChecking: false, isUpdateAvailable: false, @@ -454,6 +456,13 @@ export default function UserProfile() { }; return (
+ refetchUser()} + onClose={() => setEditProfilePic(false)} + onDelete={() => refetchUserData()} + onRefetch={() => refetchUserData()} + />
@@ -464,6 +473,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 1bbe494b9ed..3dbb637e11a 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/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index df2997abe2e..cda5fbfe671 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -87,7 +87,14 @@ export default function AuthUserProvider({ children, unauthorized }: Props) { } return ( - + {!res.ok || !user ? unauthorized : children} ); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index e53342e3d7c..686201043ed 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -304,6 +304,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",