From e73cfc78d02066fe858bb8e8837aa20d50fc7972 Mon Sep 17 00:00:00 2001 From: Steve Tsala <45661418+SteveGT96@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:34:35 +0100 Subject: [PATCH 1/2] OH2-215 | Add toggle expand table component data shorcut(ctrl+space) (#509) * update: Add toggle expand table component data shorcut(ctrl+space) * update: Add toggle expand all button to table component * update: Update expand/collapse all table rows --- src/components/accessories/table/Table.tsx | 13 ++++++++ .../accessories/table/TableBodyRow.tsx | 14 +++----- src/components/accessories/table/styles.scss | 31 +++++++++++------- src/components/accessories/table/types.ts | 1 + src/resources/i18n/en.json | 32 ++++++++++--------- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/components/accessories/table/Table.tsx b/src/components/accessories/table/Table.tsx index a2dacb3c9..a5227689e 100644 --- a/src/components/accessories/table/Table.tsx +++ b/src/components/accessories/table/Table.tsx @@ -32,6 +32,8 @@ import { import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog"; import { useTranslation } from "react-i18next"; import warningIcon from "../../../assets/warning-icon.png"; +import SmallButton from "../smallButton/SmallButton"; +import Button from "../button/Button"; const Table: FunctionComponent = ({ rowData, @@ -64,6 +66,7 @@ const Table: FunctionComponent = ({ const [openDeleteConfirmation, setOpenDeleteConfirmation] = useState(false); const [openCancelConfirmation, setOpenCancelConfirmation] = useState(false); const [currentRow, setCurrentRow] = useState({} as any); + const [expanded, setExpanded] = useState(false); const handleChangePage = (event: unknown, newPage: number) => { setPage(newPage); }; @@ -224,9 +227,18 @@ const Table: FunctionComponent = ({ setOpenCancelConfirmation(false); }; + const handleExpand = () => { + setExpanded(!expanded); + }; + return ( <> +
+ +
@@ -276,6 +288,7 @@ const Table: FunctionComponent = ({ showEmptyCell={showEmptyCell} renderCellDetails={renderItemDetails} detailColSpan={detailColSpan} + expanded={expanded} /> ))} diff --git a/src/components/accessories/table/TableBodyRow.tsx b/src/components/accessories/table/TableBodyRow.tsx index ca4a753c5..0467d3889 100644 --- a/src/components/accessories/table/TableBodyRow.tsx +++ b/src/components/accessories/table/TableBodyRow.tsx @@ -20,19 +20,13 @@ const TableBodyRow: FunctionComponent = ({ renderCellDetails, coreRow, detailColSpan, + expanded, }) => { const [open, setOpen] = React.useState(false); - const isPrintMode = useMediaQuery("print"); - useHotkeys("ctrl+p", async (event, handler) => { - setOpen(true); - await sleep(1000); - }); useEffect(() => { - if (!isPrintMode) { - setOpen(false); - } - }, [isPrintMode]); + setOpen(expanded ?? open); + }, [expanded]); return ( <> @@ -68,7 +62,7 @@ const TableBodyRow: FunctionComponent = ({ colSpan={detailColSpan ?? 6} > (row: T) => any; coreRow?: any; detailColSpan?: number; + expanded?: boolean; } export type TActions = diff --git a/src/resources/i18n/en.json b/src/resources/i18n/en.json index d093039a0..ccc5dea76 100644 --- a/src/resources/i18n/en.json +++ b/src/resources/i18n/en.json @@ -444,7 +444,9 @@ "thisyear": "This Year", "lastyear": "Last Year" }, - "continue": "Continue ?" + "continue": "Continue ?", + "collapse_all": "Collapse All", + "expand_all": "Expand All" }, "permission": { "denied": "Permission denied", @@ -546,11 +548,11 @@ "statusmustbedone": "Exam status must be 'DONE' if results are provided", "changestatus": "Change Exam Status", "changelabstatusto": "The status of the exam {{code}} will be changed to {{status}}", - "statuses" : { - "DRAFT" : "DRAFT", - "ALL" : "ALL", - "OPEN" : "OPEN", - "DONE" : "DONE" + "statuses": { + "DRAFT": "DRAFT", + "ALL": "ALL", + "OPEN": "OPEN", + "DONE": "DONE" } }, "admission": { @@ -725,15 +727,15 @@ "elderly": "Elderly" }, "lab": { - "undefined" : {"txt": "Undefined"}, - "blood": {"txt": "Blood"}, - "cfs": {"txt": "CFS"}, - "film": {"txt": "FILM"}, - "sputum":{"txt": "Sputum"}, - "stool":{"txt": "Stool"}, - "swabs":{"txt": "Swabs"}, - "tissues":{"txt": "Tissues"}, - "urine":{"txt": "Urine"} + "undefined": { "txt": "Undefined" }, + "blood": { "txt": "Blood" }, + "cfs": { "txt": "CFS" }, + "film": { "txt": "FILM" }, + "sputum": { "txt": "Sputum" }, + "stool": { "txt": "Stool" }, + "swabs": { "txt": "Swabs" }, + "tissues": { "txt": "Tissues" }, + "urine": { "txt": "Urine" } } } } From 9c90d578d38f4846bb986d1b1aec725c4fe52cb3 Mon Sep 17 00:00:00 2001 From: Steve Tsala <45661418+SteveGT96@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:32:08 +0100 Subject: [PATCH 2/2] OH2-229 | Add crop profile picture function (#510) * feature(OH2-229): Add crop profile picture function * refactor: Remove unused imports in Cropper component * update: Update photo cropper * fix: Fix e2e tests * update: Update image cropper and max file upload limit * update(OH2-229): Update max file upload limit to 648 KB * update: Update max size computation * update: Add profile image compression * fix: Fix patient profile image not showing * refactor: Reformat code --- cypress/downloads/downloads.htm | Bin 5082 -> 0 bytes .../integration/edit_patient_activity.spec.js | 9 +- .../integration/new_patient_activity.spec.js | 4 +- package-lock.json | 58 ++++++++--- package.json | 1 + .../profilePicture/ProfilePicture.tsx | 67 ++++++++++-- .../accessories/profilePicture/consts.ts | 1 + .../accessories/profilePicture/utils.ts | 98 ++++++++++++------ .../ProfilePictureCropper.tsx | 28 +++++ .../profilePictureCropper/styles.scss | 14 +++ .../profilePictureCropper/styles.ts | 47 +++++++++ .../profilePictureCropper/types.ts | 8 ++ .../profilePictureCropper/utils.ts | 83 +++++++++++++++ src/index.tsx | 2 +- 14 files changed, 365 insertions(+), 55 deletions(-) delete mode 100644 cypress/downloads/downloads.htm create mode 100644 src/components/accessories/profilePicture/consts.ts create mode 100644 src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx create mode 100644 src/components/accessories/profilePictureCropper/styles.scss create mode 100644 src/components/accessories/profilePictureCropper/styles.ts create mode 100644 src/components/accessories/profilePictureCropper/types.ts create mode 100644 src/components/accessories/profilePictureCropper/utils.ts diff --git a/cypress/downloads/downloads.htm b/cypress/downloads/downloads.htm deleted file mode 100644 index fe2280c656fce48d195948015763c98d28629945..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5082 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 V)53&?_RlTl@A2UGPGbFY`VU!EuZREu diff --git a/cypress/integration/edit_patient_activity.spec.js b/cypress/integration/edit_patient_activity.spec.js index 04cdb3120..47beddfe0 100644 --- a/cypress/integration/edit_patient_activity.spec.js +++ b/cypress/integration/edit_patient_activity.spec.js @@ -8,7 +8,7 @@ describe("EditPatientActivity spec", () => { cy.get("[class=editPatient]"); }); - it.skip("should have access to the user credentials", () => { }); + it.skip("should have access to the user credentials", () => {}); it("should have a PatientDataForm as a child component", () => { cy.get("[class=patientDataForm]"); @@ -44,6 +44,8 @@ describe("EditPatientActivity spec", () => { { force: true } ); + cy.get("[class=MuiDialogContent-root]").contains("Confirm").click(); + cy.wait(1000); cy.get("[class=profilePicture]") .find("img") @@ -79,11 +81,10 @@ describe("EditPatientActivity spec", () => { it("should not leave on the Cancel button click, if the Cancel button of the Cancel Dialog is click", () => { cy.get("[id=firstName]").clear().type("Marcelo"); cy.get("[class=patientDataForm]").contains("Cancel").click(); - cy.url().then(url => { + cy.url().then((url) => { cy.get("div.dialog__buttonSet").contains("Keep").click(); - cy.url().should('eq', url); + cy.url().should("eq", url); }); //cy.get("[id=firstName]").should("have.value", "Antonio Carlos"); }); - }); diff --git a/cypress/integration/new_patient_activity.spec.js b/cypress/integration/new_patient_activity.spec.js index 158b8d5cd..91fc300cc 100644 --- a/cypress/integration/new_patient_activity.spec.js +++ b/cypress/integration/new_patient_activity.spec.js @@ -8,7 +8,7 @@ describe("NewPatientActivity spec", () => { cy.get("[class=newPatient]"); }); - it.skip("should have access to the user credentials", () => { }); + it.skip("should have access to the user credentials", () => {}); it("should have a PatientDataForm as a child component", () => { cy.get("[class=patientDataForm]"); @@ -44,6 +44,8 @@ describe("NewPatientActivity spec", () => { { force: true } ); + cy.get("[class=MuiDialogContent-root]").contains("Confirm").click(); + cy.wait(1000); cy.get("[class=profilePicture]") .find("img") diff --git a/package-lock.json b/package-lock.json index f6cab6d5c..f81a919e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@types/react-router": "^5.1.19", "@types/react-router-dom": "^5.3.3", "@types/yup": "^0.29.0", + "browser-image-compression": "^2.0.2", "chart.js": "^3.9.1", "classnames": "^2.2.6", "date-fns": "^2.16.1", @@ -15776,6 +15777,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -15874,6 +15883,12 @@ "pako": "~1.0.5" } }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/browserslist": { "version": "4.21.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", @@ -29568,12 +29583,6 @@ "node": ">=6" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "node_modules/parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", @@ -32010,7 +32019,8 @@ }, "node_modules/react-image-crop": { "version": "9.1.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-9.1.1.tgz", + "integrity": "sha512-n7O3Cn7RuZJF8ooau2atwCGWZHqO7akl/pa6IR+1jWy5X0x4enTOCWOeswYhcBOv0ohmDHctAlf6mcUuSSwsow==", "dependencies": { "clsx": "^1.1.1" }, @@ -36312,6 +36322,11 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -49526,6 +49541,14 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, + "browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "requires": { + "uzip": "0.20201231.0" + } + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -49610,6 +49633,14 @@ "dev": true, "requires": { "pako": "~1.0.5" + }, + "dependencies": { + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + } } }, "browserslist": { @@ -59803,12 +59834,6 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", @@ -61406,6 +61431,8 @@ }, "react-image-crop": { "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-9.1.1.tgz", + "integrity": "sha512-n7O3Cn7RuZJF8ooau2atwCGWZHqO7akl/pa6IR+1jWy5X0x4enTOCWOeswYhcBOv0ohmDHctAlf6mcUuSSwsow==", "requires": { "clsx": "^1.1.1" } @@ -64610,6 +64637,11 @@ "integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==", "dev": true }, + "uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index 21b250ba7..3eee8fa25 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react-router": "^5.1.19", "@types/react-router-dom": "^5.3.3", "@types/yup": "^0.29.0", + "browser-image-compression": "^2.0.2", "chart.js": "^3.9.1", "classnames": "^2.2.6", "date-fns": "^2.16.1", diff --git a/src/components/accessories/profilePicture/ProfilePicture.tsx b/src/components/accessories/profilePicture/ProfilePicture.tsx index c2bbde2f3..fc9b3b951 100644 --- a/src/components/accessories/profilePicture/ProfilePicture.tsx +++ b/src/components/accessories/profilePicture/ProfilePicture.tsx @@ -14,6 +14,7 @@ import AddRoundedIcon from "@material-ui/icons/AddRounded"; import PhotoCameraIcon from "@material-ui/icons/PhotoCamera"; import AddPhotoAlternateIcon from "@material-ui/icons/AddPhotoAlternate"; import React, { + ChangeEvent, FunctionComponent, useCallback, useEffect, @@ -25,9 +26,15 @@ import Webcam from "../../accessories/webcam/Webcam"; import profilePicturePlaceholder from "../../../assets/profilePicturePlaceholder.png"; import "./styles.scss"; import { IProps } from "./types"; -import { handlePictureSelection, preprocessImage } from "./utils"; +import { + extractPictureFromSelection, + handlePictureSelection, + preprocessImage, +} from "./utils"; import classNames from "classnames"; import { GridCloseIcon } from "@material-ui/data-grid"; +import { ProfilePictureCropper } from "../profilePictureCropper/ProfilePictureCropper"; +import { isEmpty } from "lodash"; export const ProfilePicture: FunctionComponent = ({ isEditable, @@ -42,18 +49,25 @@ export const ProfilePicture: FunctionComponent = ({ original: "", }); - const [showError, setShowError] = React.useState(""); - const [showModal, setShowModal] = React.useState(false); - const [showWebcam, setShowWebcam] = React.useState(false); + const [showError, setShowError] = useState(""); + const [showModal, setShowModal] = useState(false); + const [showWebcam, setShowWebcam] = useState(false); + const [showCropper, setShowCropper] = useState(false); + const [fromFileSystem, setFromFileSystem] = useState(false); + const [pictureToResize, setPictureToResize] = useState(""); const { t } = useTranslation(); const handleCloseError = () => { + removePicture(); setShowError(""); }; useEffect(() => { if (preLoadedPicture) { - preprocessImage(setPicture, preLoadedPicture); + setPicture({ + preview: "data:image/jpeg;base64," + preLoadedPicture, + original: preLoadedPicture, + }); } }, [preLoadedPicture]); @@ -63,6 +77,13 @@ export const ProfilePicture: FunctionComponent = ({ } }, [onChange, picture.original]); + useEffect(() => { + if (!showModal && !isEmpty(pictureToResize) && fromFileSystem) { + setFromFileSystem(false); + openCropper(); + } + }, [pictureToResize]); + const pictureInputRef = useRef(null); const choosePicture = () => pictureInputRef.current?.click(); @@ -75,8 +96,11 @@ export const ProfilePicture: FunctionComponent = ({ const openWebcam = () => setShowWebcam(true); const closeWebcam = () => setShowWebcam(false); + const openCropper = () => setShowCropper(true); + const closeCropper = () => setShowCropper(false); const removePicture = () => { + setPictureToResize(""); setPicture({ preview: profilePicturePlaceholder, original: "", @@ -86,14 +110,36 @@ export const ProfilePicture: FunctionComponent = ({ } }; + const handleCropped = useCallback( + (value: string) => { + preprocessImage(setPicture, value, setShowError); + closeCropper(); + }, + [setPicture] + ); + const confirmWebcamPicture = useCallback( (image: string) => { - preprocessImage(setPicture, image); + preprocessImage(setPicture, image, setShowError); closeModal(); }, [setPicture] ); + const handleChange = useCallback( + () => (e: ChangeEvent) => { + setFromFileSystem(true); + extractPictureFromSelection(setPictureToResize)(e); + }, + [setPictureToResize] + ); + + const handleReset = () => { + closeCropper(); + removePicture(); + pictureInputRef.current?.click(); + }; + useEffect(() => { if (shouldReset && resetCallback) { removePicture(); @@ -103,13 +149,20 @@ export const ProfilePicture: FunctionComponent = ({ return (
+
{ const canvas = document.createElement("canvas"); @@ -24,45 +27,64 @@ const createPreview = (img: HTMLImageElement) => { return canvas.toDataURL("image/jpeg", 0.7); // get the data from canvas as 70% JPG }; -export const handlePictureSelection = ( - setPicture: Dispatch< - SetStateAction<{ - preview: string; - original: string; - }> - >, setShowError: React.Dispatch>, maxFileUpload: number -) => (e: ChangeEvent): void => { - const newPic = e.target.files && e.target.files[0]; - if (getFileSize(newPic, maxFileUpload)) { +export const handlePictureSelection = + ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, + setShowError: React.Dispatch> + ) => + (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; if (newPic) { const dataURLReader = new FileReader(); dataURLReader.onload = (e) => { const pictureURI = e.target?.result; if (typeof pictureURI === "string") { - preprocessImage(setPicture, pictureURI); + preprocessImage(setPicture, pictureURI, setShowError); } }; dataURLReader.readAsDataURL(newPic); } - } else { - setShowError("File is too big! (Max upload file is " + maxFileUpload / 1000 + " KB)"); - return; - } -}; + }; -export const getFileSize = (file: File | null, maxFileUpload: number): boolean => ( - !file || file.size > maxFileUpload ? false : true -); +export const extractPictureFromSelection = + (setPictureToResize: React.Dispatch>) => + (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; + if (newPic) { + const dataURLReader = new FileReader(); + dataURLReader.onload = (e) => { + const pictureURI = e.target?.result; + if (typeof pictureURI === "string") { + setPictureToResize(pictureURI); + } + }; + dataURLReader.readAsDataURL(newPic); + } + }; + +export const getFileSize = ( + file: File | null, + maxFileUpload: number +): boolean => (!file || file.size > maxFileUpload ? false : true); + +export const isValidSize = (file: Blob, maxFileUpload: number): boolean => + file.size > maxFileUpload ? false : true; -export const preprocessImage = ( +export const preprocessImage = async ( setPicture: Dispatch< SetStateAction<{ preview: string; original: string; }> >, - picture: string -): void => { + picture: string, + setShowError?: React.Dispatch> +) => { let pictureURI = ""; let pictureData = ""; if (picture.includes("data:")) { @@ -72,12 +94,30 @@ export const preprocessImage = ( pictureURI = "data:image/jpeg;base64," + picture; pictureData = picture; } - - const image = new Image(); - image.src = pictureURI; - - image.onload = function () { - const preview = createPreview(image); - setPicture({ original: pictureData, preview }); + let file = await imageCompression.getFilefromDataUrl(picture, "avatar"); + const compressionOptions = { + maxSizeMB: MAX_FILE_UPLOAD_SIZE / 1024 / 1024, + useWebWorker: true, }; + file = await imageCompression(file, compressionOptions); + pictureURI = await imageCompression.getDataUrlFromFile(file); + pictureData = pictureURI.split(",")[1]; + if (file.size < MAX_FILE_UPLOAD_SIZE) { + const image = new Image(); + image.src = pictureURI; + + image.onload = function () { + const preview = createPreview(image); + setPicture({ original: pictureData, preview }); + }; + } else { + if (setShowError) { + setShowError( + "File is too big! (Max upload file is " + + MAX_FILE_UPLOAD_SIZE / 1024 + + " KB)" + ); + } + return; + } }; diff --git a/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx b/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx new file mode 100644 index 000000000..9b6c6c391 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/ProfilePictureCropper.tsx @@ -0,0 +1,28 @@ +import React, { FunctionComponent } from "react"; +import "./styles.scss"; +import { IProps } from "./types"; +import { Dialog, DialogContent, DialogContentText } from "@material-ui/core"; +import ImageResize from "../imageResize/ImageResize"; + +export const ProfilePictureCropper: FunctionComponent = ({ + picture, + onSave, + onReset, + open, +}) => { + return ( +
+ + + + + + + +
+ ); +}; diff --git a/src/components/accessories/profilePictureCropper/styles.scss b/src/components/accessories/profilePictureCropper/styles.scss new file mode 100644 index 000000000..f3ef6c9d7 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/styles.scss @@ -0,0 +1,14 @@ +@import "../../../styles/variables"; + +.croppedProfilePicture { + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} +.croppedProfilePicture img { + width: 100%; + height: auto; + overflow: hidden; + object-fit: cover; +} diff --git a/src/components/accessories/profilePictureCropper/styles.ts b/src/components/accessories/profilePictureCropper/styles.ts new file mode 100644 index 000000000..53f748415 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/styles.ts @@ -0,0 +1,47 @@ +import { Theme } from "@material-ui/core"; +import { makeStyles } from "@material-ui/styles"; + +export const useStyles = makeStyles((theme: Theme) => ({ + cropContainer: { + position: "relative", + width: "100%", + height: 200, + background: "#333", + [theme.breakpoints.up("sm")]: { + height: 400, + }, + }, + cropButton: { + flexShrink: 0, + marginLeft: 16, + }, + controls: { + padding: 16, + display: "flex", + flexDirection: "column", + alignItems: "stretch", + [theme.breakpoints.up("sm")]: { + flexDirection: "row", + alignItems: "center", + }, + }, + sliderContainer: { + display: "flex", + flex: "1", + alignItems: "center", + }, + sliderLabel: { + [theme.breakpoints.down("xs")]: { + minWidth: 65, + }, + }, + slider: { + padding: "22px 0px", + marginLeft: 32, + [theme.breakpoints.up("sm")]: { + flexDirection: "row", + alignItems: "center", + margin: "0 16px", + }, + }, +})); diff --git a/src/components/accessories/profilePictureCropper/types.ts b/src/components/accessories/profilePictureCropper/types.ts new file mode 100644 index 000000000..075ee8e0e --- /dev/null +++ b/src/components/accessories/profilePictureCropper/types.ts @@ -0,0 +1,8 @@ +import { CSSProperties } from "react"; + +export interface IProps { + open: boolean; + picture: string; + onSave: (image: string) => void; + onReset: () => void; +} diff --git a/src/components/accessories/profilePictureCropper/utils.ts b/src/components/accessories/profilePictureCropper/utils.ts new file mode 100644 index 000000000..105fd6fb7 --- /dev/null +++ b/src/components/accessories/profilePictureCropper/utils.ts @@ -0,0 +1,83 @@ +import { ChangeEvent, Dispatch, SetStateAction } from "react"; + +const createPreview = (img: HTMLImageElement) => { + const canvas = document.createElement("canvas"); + let width = img.width; + let height = img.height; + // calculate the width and height, constraining the proportions + if (width > height) { + if (width > 180) { + height = Math.round((height *= 180 / width)); + width = 180; + } + } else { + if (height > 160) { + width = Math.round((width *= 160 / height)); + height = 160; + } + } + // resize the canvas and draw the image data into it + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + ctx?.drawImage(img, 0, 0, width, height); + return canvas.toDataURL("image/jpeg", 0.7); // get the data from canvas as 70% JPG +}; + +export const handlePictureSelection = ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, setShowError: React.Dispatch>, maxFileUpload: number +) => (e: ChangeEvent): void => { + const newPic = e.target.files && e.target.files[0]; + if (getFileSize(newPic, maxFileUpload)) { + if (newPic) { + const dataURLReader = new FileReader(); + dataURLReader.onload = (e) => { + const pictureURI = e.target?.result; + if (typeof pictureURI === "string") { + preprocessImage(setPicture, pictureURI); + } + }; + dataURLReader.readAsDataURL(newPic); + } + } else { + setShowError("File is too big! (Max upload file is " + maxFileUpload / 1000 + " KB)"); + return; + } +}; + +export const getFileSize = (file: File | null, maxFileUpload: number): boolean => ( + !file || file.size > maxFileUpload ? false : true +); + +export const preprocessImage = ( + setPicture: Dispatch< + SetStateAction<{ + preview: string; + original: string; + }> + >, + picture: string +): void => { + let pictureURI = ""; + let pictureData = ""; + if (picture.includes("data:")) { + pictureURI = picture; + pictureData = picture.split(",")[1]; + } else { + pictureURI = "data:image/jpeg;base64," + picture; + pictureData = picture; + } + + const image = new Image(); + image.src = pictureURI; + + image.onload = function () { + const preview = createPreview(image); + setPicture({ original: pictureData, preview }); + }; +}; diff --git a/src/index.tsx b/src/index.tsx index 76d3ec29a..d37ee229d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -35,7 +35,7 @@ import layouts from "./state/layouts/reducer"; import dashboard from "./state/dashboard/reducer"; if (process.env.REACT_APP_USE_MOCK_API) { - //console.log("Using mocked api"); + console.log("Using mocked api"); makeServer(); }