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"