From c3c472d4d7a4681113a193fca0939311f2386bb2 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Mon, 26 Oct 2020 17:55:17 +1100 Subject: [PATCH 1/4] Add crop and upload to cloudinary --- web/package.json | 3 + web/src/cascade | 2 +- web/src/components/PartForm/ImageUploader.js | 105 +++++++++++++++++++ web/src/components/PartForm/PartForm.js | 5 +- yarn.lock | 52 ++++++++- 5 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 web/src/components/PartForm/ImageUploader.js diff --git a/web/package.json b/web/package.json index db3c665..a9942bc 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,7 @@ "@redwoodjs/forms": "^0.19.2", "@redwoodjs/router": "^0.19.2", "@redwoodjs/web": "^0.19.2", + "cloudinary-react": "^1.6.7", "controlkit": "^0.1.9", "golden-layout": "^1.5.9", "jquery": "^3.5.1", @@ -28,6 +29,8 @@ "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-dropzone": "^11.2.1", + "react-image-crop": "^8.6.6", "rich-markdown-editor": "^11.0.2", "styled-components": "^5.2.0", "three": "^0.118.3" diff --git a/web/src/cascade b/web/src/cascade index 62f9612..e634591 160000 --- a/web/src/cascade +++ b/web/src/cascade @@ -1 +1 @@ -Subproject commit 62f961293d72558e59cdcbe0707ef15a06d30c12 +Subproject commit e634591e27dd41fec1638b278be3c298c6ab4b5a diff --git a/web/src/components/PartForm/ImageUploader.js b/web/src/components/PartForm/ImageUploader.js new file mode 100644 index 0000000..476293f --- /dev/null +++ b/web/src/components/PartForm/ImageUploader.js @@ -0,0 +1,105 @@ +import React, { useCallback, useState } from "react"; +import { useDropzone } from "react-dropzone"; +import Button from "@material-ui/core/Button"; +import axios from 'axios' +import ReactCrop from 'react-image-crop' +import { Dialog } from '@material-ui/core' +import 'react-image-crop/dist/ReactCrop.css' + +const CLOUDINARY_UPLOAD_PRESET = process.env.GATSBY_PROD_PRESET || "dev_preset"; +const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload"; + +export default function ImageUploader({ onImageUpload }) { + const [isModalOpen, setIsModalOpen] = useState(false) + const [file, setFile] = useState() + const [crop, setCrop] = useState({ + aspect: 16 / 9, + unit: '%', + width: 100, + }); + async function handleImageUpload() { + var image = new Image(); + image.src = file + const croppedFile = await getCroppedImg(image, crop, 'avatar') + console.log(croppedFile) + const imageData = new FormData(); + imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET); + imageData.append('file', croppedFile); + let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData) + try { + const { data } = await upload + if (data && data.public_id !== "") { + onImageUpload({cloudinaryPublicId: data.public_id}) + } + } catch (e) { + console.error('ERROR', e) + } + } + // Drag and Drop + const onDrop = useCallback(acceptedFiles => { + setIsModalOpen(true) + const fileReader = new FileReader() + fileReader.onload = () => { + setFile(fileReader.result) + } + fileReader.readAsDataURL(acceptedFiles[0]) + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + return ( +
+
+ + {/* */} + +
+ Drop files here ... + or + upload + +
+
+ setIsModalOpen(false)} + > +
+ setCrop(newCrop)} /> + +
+
+
+ ); +} + +function getCroppedImg(image, crop, fileName) { + const canvas = document.createElement('canvas'); + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + canvas.width = crop.width; + canvas.height = crop.height; + const ctx = canvas.getContext('2d'); + + ctx.drawImage( + image, + crop.x * scaleX, + crop.y * scaleY, + crop.width * scaleX, + crop.height * scaleY, + 0, + 0, + crop.width, + crop.height, + ); + + // As Base64 string + // const base64Image = canvas.toDataURL('image/jpeg'); + + // As a blob + return new Promise((resolve, reject) => { + canvas.toBlob(blob => { + blob.name = fileName; + resolve(blob); + }, 'image/jpeg', 1); + }); +} diff --git a/web/src/components/PartForm/PartForm.js b/web/src/components/PartForm/PartForm.js index c300d8c..b5aa2cd 100644 --- a/web/src/components/PartForm/PartForm.js +++ b/web/src/components/PartForm/PartForm.js @@ -4,12 +4,13 @@ import { FieldError, Label, TextField, - TextAreaField, Submit, } from '@redwoodjs/forms' import { useState } from 'react'; import { navigate, routes } from '@redwoodjs/router' import { useFlash } from '@redwoodjs/web' +import ImageUploader from './ImageUploader.js' + import Editor from "rich-markdown-editor"; @@ -34,6 +35,8 @@ const PartForm = (props) => { return (
+ {console.log('yo', yo)}} /> + Date: Mon, 26 Oct 2020 20:55:16 +1100 Subject: [PATCH 2/4] Integrate image uploader with new part and make image editable that is to say you can easily pick another image if you didn't like the first. --- web/src/components/PartForm/ImageUploader.js | 44 +++++++++++++------- web/src/components/PartForm/PartForm.js | 21 ++-------- web/src/components/Parts/Parts.js | 13 +++++- web/src/components/Svg/Svg.js | 9 ++-- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/web/src/components/PartForm/ImageUploader.js b/web/src/components/PartForm/ImageUploader.js index 476293f..4809ec0 100644 --- a/web/src/components/PartForm/ImageUploader.js +++ b/web/src/components/PartForm/ImageUploader.js @@ -4,23 +4,25 @@ import Button from "@material-ui/core/Button"; import axios from 'axios' import ReactCrop from 'react-image-crop' import { Dialog } from '@material-ui/core' +import { Image as CloudinaryImage } from 'cloudinary-react' import 'react-image-crop/dist/ReactCrop.css' +import Svg from 'src/components/Svg/Svg.js' -const CLOUDINARY_UPLOAD_PRESET = process.env.GATSBY_PROD_PRESET || "dev_preset"; -const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload"; +const CLOUDINARY_UPLOAD_PRESET = "CadHub_project_images"; +const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload/?custom_coordinates=10,10,20,20"; -export default function ImageUploader({ onImageUpload }) { +export default function ImageUploader({ onImageUpload, imageUrl }) { const [isModalOpen, setIsModalOpen] = useState(false) const [file, setFile] = useState() + const [cloudinaryId, setCloudinaryId] = useState(imageUrl) + const [imageObj, setImageObj] = useState() const [crop, setCrop] = useState({ aspect: 16 / 9, unit: '%', width: 100, }); async function handleImageUpload() { - var image = new Image(); - image.src = file - const croppedFile = await getCroppedImg(image, crop, 'avatar') + const croppedFile = await getCroppedImg(imageObj, crop, 'avatar') console.log(croppedFile) const imageData = new FormData(); imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET); @@ -30,6 +32,8 @@ export default function ImageUploader({ onImageUpload }) { const { data } = await upload if (data && data.public_id !== "") { onImageUpload({cloudinaryPublicId: data.public_id}) + setCloudinaryId(data.public_id) + setIsModalOpen(false) } } catch (e) { console.error('ERROR', e) @@ -47,24 +51,35 @@ export default function ImageUploader({ onImageUpload }) { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); return ( -
+
+ {cloudinaryId && } - {/* */} - -
+ {cloudinaryId &&
+ +
} + {!cloudinaryId && } + {!cloudinaryId &&
Drop files here ... - or + or upload -
+
}
setIsModalOpen(false)} >
- setCrop(newCrop)} /> + setImageObj(image)} onChange={newCrop => setCrop(newCrop)} />
@@ -79,7 +94,6 @@ function getCroppedImg(image, crop, fileName) { canvas.width = crop.width; canvas.height = crop.height; const ctx = canvas.getContext('2d'); - ctx.drawImage( image, crop.x * scaleX, @@ -89,7 +103,7 @@ function getCroppedImg(image, crop, fileName) { 0, 0, crop.width, - crop.height, + crop.height ); // As Base64 string diff --git a/web/src/components/PartForm/PartForm.js b/web/src/components/PartForm/PartForm.js index b5aa2cd..daecc16 100644 --- a/web/src/components/PartForm/PartForm.js +++ b/web/src/components/PartForm/PartForm.js @@ -17,11 +17,13 @@ import Editor from "rich-markdown-editor"; const PartForm = (props) => { const { addMessage } = useFlash() const [description, setDescription] = useState(props?.part?.description) + const [imageUrl, setImageUrl] = useState(props?.part?.mainImage) const onSubmit = async (data, e) => { await props.onSave({ ...data, description, + mainImage: imageUrl }, props?.part?.id) const shouldOpenIde = e?.nativeEvent?.submitter?.dataset?.openIde if(shouldOpenIde) { @@ -35,8 +37,6 @@ const PartForm = (props) => { return (
- {console.log('yo', yo)}} /> - { /> - - - + setImageUrl(cloudinaryPublicId)} /> +