From 0c3ec980626697cc840b2566369184ab1e658a36 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Thu, 12 Aug 2021 10:14:11 +0530 Subject: [PATCH 1/3] added image-uploader component and refactored profile settings page --- components/Avatar.tsx | 7 +- components/ImageUploader.tsx | 230 +++++++++++++++++++++++++++++++++++ package.json | 1 + pages/[user]/book.tsx | 1 - pages/settings/profile.tsx | 57 ++++++--- styles/globals.css | 8 +- yarn.lock | 18 +++ 7 files changed, 301 insertions(+), 21 deletions(-) create mode 100644 components/ImageUploader.tsx diff --git a/components/Avatar.tsx b/components/Avatar.tsx index 3d83232a..ce13d974 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -1,13 +1,18 @@ import { useState } from "react"; import md5 from '../lib/md5'; -export default function Avatar({ user, className = '', fallback }: { +export default function Avatar({ user, className = '', fallback, imageSrc = '' }: { user: any; className?: string; fallback?: JSX.Element; + imageSrc?: string; }) { const [gravatarAvailable, setGravatarAvailable] = useState(true); + if (imageSrc) { + return Avatar; + } + if (user.avatar) { return Avatar; } diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx new file mode 100644 index 00000000..700edeef --- /dev/null +++ b/components/ImageUploader.tsx @@ -0,0 +1,230 @@ +import Cropper from "react-easy-crop"; +import { useState, useCallback, useRef } from "react"; + +export default function ImageUploader({target, id, buttonMsg, handleAvatarChange, imageRef}){ + const imageFileRef = useRef(); + const [imageDataUrl, setImageDataUrl] = useState(); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(); + const [rotation] = useState(1); + const [crop, setCrop] = useState({ x: 0, y: 0 }) + const [zoom, setZoom] = useState(1) + const [imageLoaded, setImageLoaded] = useState(false); + const [isImageShown, setIsImageShown] = useState(false); + const [shownImage, setShownImage] = useState(); + const [imageUploadModalOpen, setImageUploadModalOpen] = useState(false); + + // TODO + // PUSH cropped image to the database in column = target + const openUploaderModal = () => { + imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false) + setImageUploadModalOpen(!imageUploadModalOpen) + } + + const closeImageUploadModal = () => { + setImageUploadModalOpen(false); + }; + + async function ImageUploadHandler() { + console.log(imageFileRef.current.files[0]); + const img = await readFile(imageFileRef.current.files[0]); + console.log(img); + setImageDataUrl(img); + CropHandler(); + } + + const readFile = (file) => { + return new Promise((resolve) => { + const reader = new FileReader() + reader.addEventListener('load', () => resolve(reader.result), false) + reader.readAsDataURL(file) + }) + } + + const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { + setCroppedAreaPixels(croppedAreaPixels) + + }, []) + + const CropHandler = () => { + setCrop({ x: 0, y: 0 }); + setZoom(1); + setImageLoaded(true); + } + + const createImage = (url) => + new Promise((resolve, reject) => { + const image = new Image() + image.addEventListener('load', () => resolve(image)) + image.addEventListener('error', error => reject(error)) + image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox + image.src = url + }) + + function getRadianAngle(degreeValue) { + return (degreeValue * Math.PI) / 180 + } + + async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { + const image = await createImage(imageSrc) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + const maxSize = Math.max(image.width, image.height) + const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)) + + // set each dimensions to double largest dimension to allow for a safe area for the + // image to rotate in without being clipped by canvas context + canvas.width = safeArea + canvas.height = safeArea + + // translate canvas context to a central location on image to allow rotating around the center. + ctx.translate(safeArea / 2, safeArea / 2) + ctx.rotate(getRadianAngle(rotation)) + ctx.translate(-safeArea / 2, -safeArea / 2) + + // draw rotated image and store data. + ctx.drawImage( + image, + safeArea / 2 - image.width * 0.5, + safeArea / 2 - image.height * 0.5 + ) + const data = ctx.getImageData(0, 0, safeArea, safeArea) + + // set canvas width to final desired crop size - this will clear existing context + canvas.width = pixelCrop.width + canvas.height = pixelCrop.height + + // paste generated rotate image with correct offsets for x,y crop values. + ctx.putImageData( + data, + Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), + Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) + ) + + // As Base64 string + return canvas.toDataURL('image/jpeg'); + + // As a blob + // return new Promise(resolve => { + // canvas.toBlob(file => { + // resolve(URL.createObjectURL(file)) + // }, 'image/jpeg') + // }) + } + + + const showCroppedImage = useCallback(async () => { + try { + const croppedImage = await getCroppedImg( + imageDataUrl, + croppedAreaPixels, + rotation + ) + setIsImageShown(true) + setShownImage(croppedImage) + setImageLoaded(false) + handleAvatarChange(croppedImage) + closeImageUploadModal() + } catch (e) { + console.error(e) + } + }, [croppedAreaPixels, rotation]) + + return ( +
+ + + { + imageUploadModalOpen && +
+
+ + + + +
+
+ +
+ +
+
+
+
+ {!imageLoaded && +
+ {!isImageShown && +

No {target}

+ } + {isImageShown && + {target} + } +
+ } + {imageLoaded && +
+
+ +
+
+ } + + +
+
+
+ + +
+
+
+
+ + } +
+ + ) +} \ No newline at end of file diff --git a/package.json b/package.json index a25a020f..d8ea43c7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react": "17.0.1", "react-dates": "^21.8.0", "react-dom": "17.0.1", + "react-easy-crop": "^3.5.2", "react-multi-email": "^0.5.3", "react-phone-number-input": "^3.1.21", "react-select": "^4.3.0", diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 74e2c785..df9b19c5 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -16,7 +16,6 @@ import Button from "../../components/ui/Button"; import { EventTypeCustomInputType } from "../../lib/eventTypeInput"; import Theme from "@components/Theme"; import { ReactMultiEmail } from "react-multi-email"; -import "react-multi-email/style.css"; dayjs.extend(utc); dayjs.extend(timezone); diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 30842c7b..3c06e009 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -11,6 +11,7 @@ import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; import { UsernameInput } from "../../components/ui/UsernameInput"; import ErrorAlert from "../../components/ui/alerts/Error"; +import ImageUploader from "../../components/ImageUploader"; const themeOptions = [ { value: "light", label: "Light" }, @@ -27,6 +28,7 @@ export default function Settings(props) { const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme }); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart }); + const [imageSrc, setImageSrc] = useState(''); const [hasErrors, setHasErrors] = useState(false); const [errorMessage, setErrorMessage] = useState(""); @@ -42,6 +44,16 @@ export default function Settings(props) { setSuccessModalOpen(false); }; + const handleAvatarChange = (newAvatar) => { + avatarRef.current.value = newAvatar; + var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; + nativeInputValueSetter.call(avatarRef.current, newAvatar); + var ev2 = new Event('input', { bubbles: true}); + avatarRef.current.dispatchEvent(ev2); + updateProfileHandler(ev2); + setImageSrc(newAvatar); + } + const handleError = async (resp) => { if (!resp.ok) { const error = await resp.json(); @@ -138,6 +150,33 @@ export default function Settings(props) { className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm"> +
+
+
} + imageSrc={imageSrc} + /> + + +
+
+
-
+ {/*
@@ -236,15 +275,6 @@ export default function Settings(props) { aria-hidden="true">
- {/*
-
- - -
-
*/}
@@ -254,11 +284,6 @@ export default function Settings(props) { className="relative rounded-full w-40 h-40" fallback={
} /> - {/* */}
- + */}
diff --git a/styles/globals.css b/styles/globals.css index 2ebd20a2..91c8396d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -118,7 +118,7 @@ align-content: flex-start; padding-top: 0.1rem !important; padding-bottom: 0.1rem !important; - padding-left: 0.75rem !important; + /* padding-left: 0.75rem !important; */ @apply dark:border-black border-white dark:bg-black bg-white; } @@ -127,11 +127,13 @@ } .react-multi-email.focused{ - margin: -1px; - border: 2px solid #000 !important; @apply dark:bg-black } +.react-multi-email.focused > [type='text']{ + border: 2px solid #000 !important; +} + .react-multi-email > [type='text']:focus{ box-shadow: none !important; } diff --git a/yarn.lock b/yarn.lock index 95eaaf91..bf2dc38b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4912,6 +4912,11 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-wheel@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" + integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU= + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -5538,6 +5543,14 @@ react-dom@17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" +react-easy-crop@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-3.5.2.tgz#1fc65249e82db407c8c875159589a8029a9b7a06" + integrity sha512-cwSGO/wk42XDpEyrdAcnQ6OJetVDZZO2ry1i19+kSGZQ750aN06RU9y9z95B5QI6sW3SnaWQRKv5r5GSqVV//g== + dependencies: + normalize-wheel "^1.0.1" + tslib "2.0.1" + react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" @@ -6487,6 +6500,11 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tslib@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" From 867e5823427037598e08ab0b7a6edb55a1d497ab Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Thu, 12 Aug 2021 16:08:01 +0530 Subject: [PATCH 2/3] code improvement and accept image only for image uploader input --- components/ImageUploader.tsx | 57 ++++++++++++++++++------------------ pages/settings/profile.tsx | 4 +-- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx index 700edeef..340bc7ca 100644 --- a/components/ImageUploader.tsx +++ b/components/ImageUploader.tsx @@ -6,8 +6,8 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const [imageDataUrl, setImageDataUrl] = useState(); const [croppedAreaPixels, setCroppedAreaPixels] = useState(); const [rotation] = useState(1); - const [crop, setCrop] = useState({ x: 0, y: 0 }) - const [zoom, setZoom] = useState(1) + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); const [imageLoaded, setImageLoaded] = useState(false); const [isImageShown, setIsImageShown] = useState(false); const [shownImage, setShownImage] = useState(); @@ -16,7 +16,7 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange // TODO // PUSH cropped image to the database in column = target const openUploaderModal = () => { - imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false) + imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false); setImageUploadModalOpen(!imageUploadModalOpen) } @@ -34,14 +34,14 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const readFile = (file) => { return new Promise((resolve) => { - const reader = new FileReader() - reader.addEventListener('load', () => resolve(reader.result), false) + const reader = new FileReader(); + reader.addEventListener('load', () => resolve(reader.result), false); reader.readAsDataURL(file) }) } const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { - setCroppedAreaPixels(croppedAreaPixels) + setCroppedAreaPixels(croppedAreaPixels); }, []) @@ -53,53 +53,53 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const createImage = (url) => new Promise((resolve, reject) => { - const image = new Image() - image.addEventListener('load', () => resolve(image)) - image.addEventListener('error', error => reject(error)) - image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox - image.src = url + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', error => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox + image.src = url; }) function getRadianAngle(degreeValue) { - return (degreeValue * Math.PI) / 180 + return (degreeValue * Math.PI) / 180; } async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { - const image = await createImage(imageSrc) - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') + const image = await createImage(imageSrc); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); - const maxSize = Math.max(image.width, image.height) - const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)) + const maxSize = Math.max(image.width, image.height); + const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); // set each dimensions to double largest dimension to allow for a safe area for the // image to rotate in without being clipped by canvas context - canvas.width = safeArea - canvas.height = safeArea + canvas.width = safeArea; + canvas.height = safeArea; // translate canvas context to a central location on image to allow rotating around the center. - ctx.translate(safeArea / 2, safeArea / 2) - ctx.rotate(getRadianAngle(rotation)) - ctx.translate(-safeArea / 2, -safeArea / 2) + ctx.translate(safeArea / 2, safeArea / 2); + ctx.rotate(getRadianAngle(rotation)); + ctx.translate(-safeArea / 2, -safeArea / 2); // draw rotated image and store data. ctx.drawImage( image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5 - ) - const data = ctx.getImageData(0, 0, safeArea, safeArea) + ); + const data = ctx.getImageData(0, 0, safeArea, safeArea); // set canvas width to final desired crop size - this will clear existing context - canvas.width = pixelCrop.width - canvas.height = pixelCrop.height + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; // paste generated rotate image with correct offsets for x,y crop values. ctx.putImageData( data, Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) - ) + ); // As Base64 string return canvas.toDataURL('image/jpeg'); @@ -128,7 +128,7 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange } catch (e) { console.error(e) } - }, [croppedAreaPixels, rotation]) + }, [croppedAreaPixels, rotation]); return (
@@ -205,6 +205,7 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange name={id} placeholder="Upload image" className="mt-4 cursor-pointer opacity-0 absolute" + accept="image/*" />
diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 3c06e009..380d5b49 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -46,9 +46,9 @@ export default function Settings(props) { const handleAvatarChange = (newAvatar) => { avatarRef.current.value = newAvatar; - var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; nativeInputValueSetter.call(avatarRef.current, newAvatar); - var ev2 = new Event('input', { bubbles: true}); + const ev2 = new Event('input', { bubbles: true}); avatarRef.current.dispatchEvent(ev2); updateProfileHandler(ev2); setImageSrc(newAvatar); From 6b7fbe9ecf9efbc5c6349d9de8d33a7bc2047e78 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz Date: Sat, 14 Aug 2021 09:20:11 +0530 Subject: [PATCH 3/3] removed debug remnants --- components/ImageUploader.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx index 340bc7ca..4f669670 100644 --- a/components/ImageUploader.tsx +++ b/components/ImageUploader.tsx @@ -13,8 +13,6 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const [shownImage, setShownImage] = useState(); const [imageUploadModalOpen, setImageUploadModalOpen] = useState(false); - // TODO - // PUSH cropped image to the database in column = target const openUploaderModal = () => { imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false); setImageUploadModalOpen(!imageUploadModalOpen) @@ -25,9 +23,7 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange }; async function ImageUploadHandler() { - console.log(imageFileRef.current.files[0]); const img = await readFile(imageFileRef.current.files[0]); - console.log(img); setImageDataUrl(img); CropHandler(); } @@ -103,13 +99,6 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange // As Base64 string return canvas.toDataURL('image/jpeg'); - - // As a blob - // return new Promise(resolve => { - // canvas.toBlob(file => { - // resolve(URL.createObjectURL(file)) - // }, 'image/jpeg') - // }) }