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.
This commit is contained in:
@@ -4,23 +4,25 @@ import Button from "@material-ui/core/Button";
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ReactCrop from 'react-image-crop'
|
import ReactCrop from 'react-image-crop'
|
||||||
import { Dialog } from '@material-ui/core'
|
import { Dialog } from '@material-ui/core'
|
||||||
|
import { Image as CloudinaryImage } from 'cloudinary-react'
|
||||||
import 'react-image-crop/dist/ReactCrop.css'
|
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_PRESET = "CadHub_project_images";
|
||||||
const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload";
|
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 [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [file, setFile] = useState()
|
const [file, setFile] = useState()
|
||||||
|
const [cloudinaryId, setCloudinaryId] = useState(imageUrl)
|
||||||
|
const [imageObj, setImageObj] = useState()
|
||||||
const [crop, setCrop] = useState({
|
const [crop, setCrop] = useState({
|
||||||
aspect: 16 / 9,
|
aspect: 16 / 9,
|
||||||
unit: '%',
|
unit: '%',
|
||||||
width: 100,
|
width: 100,
|
||||||
});
|
});
|
||||||
async function handleImageUpload() {
|
async function handleImageUpload() {
|
||||||
var image = new Image();
|
const croppedFile = await getCroppedImg(imageObj, crop, 'avatar')
|
||||||
image.src = file
|
|
||||||
const croppedFile = await getCroppedImg(image, crop, 'avatar')
|
|
||||||
console.log(croppedFile)
|
console.log(croppedFile)
|
||||||
const imageData = new FormData();
|
const imageData = new FormData();
|
||||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
|
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
|
||||||
@@ -30,6 +32,8 @@ export default function ImageUploader({ onImageUpload }) {
|
|||||||
const { data } = await upload
|
const { data } = await upload
|
||||||
if (data && data.public_id !== "") {
|
if (data && data.public_id !== "") {
|
||||||
onImageUpload({cloudinaryPublicId: data.public_id})
|
onImageUpload({cloudinaryPublicId: data.public_id})
|
||||||
|
setCloudinaryId(data.public_id)
|
||||||
|
setIsModalOpen(false)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('ERROR', e)
|
console.error('ERROR', e)
|
||||||
@@ -47,24 +51,35 @@ export default function ImageUploader({ onImageUpload }) {
|
|||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="m-8">
|
||||||
<div className="w-full relative" {...getRootProps()}>
|
<div className="w-full relative" {...getRootProps()}>
|
||||||
|
{cloudinaryId && <button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
|
||||||
|
<Svg name="pencil" strokeWidth={2} className="text-gray-300 h-48 w-48" />
|
||||||
|
</button>}
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{/* <Button className variant="outlined">Upload</Button> */}
|
{cloudinaryId && <div className="relative">
|
||||||
<button className="absolute inset-0"></button>
|
<CloudinaryImage
|
||||||
<div className="mt-3 text-indigo-500 border-dashed border border-indigo-500 py-8 text-center rounded-lg w-full">
|
className="object-cover w-full rounded shadow"
|
||||||
|
cloudName="irevdev"
|
||||||
|
publicId={cloudinaryId}
|
||||||
|
width="600"
|
||||||
|
crop="scale"
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
{!cloudinaryId && <button className="absolute inset-0"></button>}
|
||||||
|
{!cloudinaryId && <div className="mt-3 text-indigo-500 border-dashed border border-indigo-500 py-8 text-center rounded-lg w-full">
|
||||||
Drop files here ...
|
Drop files here ...
|
||||||
or <span className="group flex w-full items-center justify-center">
|
or <span className="group flex w-full items-center justify-center py-4">
|
||||||
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">upload</span>
|
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">upload</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<ReactCrop src={file} crop={crop} onChange={newCrop => setCrop(newCrop)} />
|
<ReactCrop src={file} crop={crop} onImageLoaded={(image) => setImageObj(image)} onChange={newCrop => setCrop(newCrop)} />
|
||||||
<Button onClick={handleImageUpload} variant="outlined">Upload</Button>
|
<Button onClick={handleImageUpload} variant="outlined">Upload</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -79,7 +94,6 @@ function getCroppedImg(image, crop, fileName) {
|
|||||||
canvas.width = crop.width;
|
canvas.width = crop.width;
|
||||||
canvas.height = crop.height;
|
canvas.height = crop.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
image,
|
image,
|
||||||
crop.x * scaleX,
|
crop.x * scaleX,
|
||||||
@@ -89,7 +103,7 @@ function getCroppedImg(image, crop, fileName) {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
crop.width,
|
crop.width,
|
||||||
crop.height,
|
crop.height
|
||||||
);
|
);
|
||||||
|
|
||||||
// As Base64 string
|
// As Base64 string
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import Editor from "rich-markdown-editor";
|
|||||||
const PartForm = (props) => {
|
const PartForm = (props) => {
|
||||||
const { addMessage } = useFlash()
|
const { addMessage } = useFlash()
|
||||||
const [description, setDescription] = useState(props?.part?.description)
|
const [description, setDescription] = useState(props?.part?.description)
|
||||||
|
const [imageUrl, setImageUrl] = useState(props?.part?.mainImage)
|
||||||
const onSubmit = async (data, e) => {
|
const onSubmit = async (data, e) => {
|
||||||
|
|
||||||
await props.onSave({
|
await props.onSave({
|
||||||
...data,
|
...data,
|
||||||
description,
|
description,
|
||||||
|
mainImage: imageUrl
|
||||||
}, props?.part?.id)
|
}, props?.part?.id)
|
||||||
const shouldOpenIde = e?.nativeEvent?.submitter?.dataset?.openIde
|
const shouldOpenIde = e?.nativeEvent?.submitter?.dataset?.openIde
|
||||||
if(shouldOpenIde) {
|
if(shouldOpenIde) {
|
||||||
@@ -35,8 +37,6 @@ const PartForm = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto mt-10">
|
<div className="max-w-7xl mx-auto mt-10">
|
||||||
<Form onSubmit={onSubmit} error={props.error}>
|
<Form onSubmit={onSubmit} error={props.error}>
|
||||||
<ImageUploader onImageUpload={(yo) => {console.log('yo', yo)}} />
|
|
||||||
|
|
||||||
<FormError
|
<FormError
|
||||||
error={props.error}
|
error={props.error}
|
||||||
wrapperClassName="rw-form-error-wrapper"
|
wrapperClassName="rw-form-error-wrapper"
|
||||||
@@ -60,21 +60,8 @@ const PartForm = (props) => {
|
|||||||
/>
|
/>
|
||||||
<FieldError name="title" className="rw-field-error" />
|
<FieldError name="title" className="rw-field-error" />
|
||||||
|
|
||||||
<Label
|
<ImageUploader onImageUpload={({cloudinaryPublicId}) => setImageUrl(cloudinaryPublicId)} />
|
||||||
name="mainImage"
|
|
||||||
className="p-0"
|
|
||||||
errorClassName="rw-label rw-label-error"
|
|
||||||
>
|
|
||||||
Main image
|
|
||||||
</Label>
|
|
||||||
<TextField
|
|
||||||
name="mainImage"
|
|
||||||
defaultValue={props.part?.mainImage}
|
|
||||||
className="rw-input"
|
|
||||||
errorClassName="rw-input rw-input-error"
|
|
||||||
validation={{ required: false }}
|
|
||||||
/>
|
|
||||||
<FieldError name="mainImage" className="rw-field-error" />
|
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMutation, useFlash } from '@redwoodjs/web'
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { Image as CloudinaryImage } from 'cloudinary-react'
|
||||||
|
|
||||||
import avatar from 'src/assets/harold.jpg'
|
import avatar from 'src/assets/harold.jpg'
|
||||||
|
|
||||||
@@ -64,11 +65,19 @@ const PartsList = ({ parts }) => {
|
|||||||
<div className="rounded-t-2xl bg-gray-900">
|
<div className="rounded-t-2xl bg-gray-900">
|
||||||
<div className="flex items-center p-2 text-indigo-200">
|
<div className="flex items-center p-2 text-indigo-200">
|
||||||
<div className="h-full absolute inset-0 text-6xl flex items-center justify-center text-indigo-700" ><span>?</span></div>
|
<div className="h-full absolute inset-0 text-6xl flex items-center justify-center text-indigo-700" ><span>?</span></div>
|
||||||
<div className="mr-4"><img src={avatar} className="rounded-full h-10 w-10" /></div>
|
<div className="mr-4">
|
||||||
|
<img src={avatar} className="rounded-full h-10 w-10" />
|
||||||
|
</div>
|
||||||
<h3>{part.title}</h3>
|
<h3>{part.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<img className="h-full" src={part.mainImage}/>
|
<CloudinaryImage
|
||||||
|
className="object-cover w-full rounded shadow"
|
||||||
|
cloudName="irevdev"
|
||||||
|
publicId={part.mainImage}
|
||||||
|
width="300"
|
||||||
|
crop="scale"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
const Svg = ({name, className: className2}) => {
|
const Svg = ({name, className: className2, strokeWidth = 2}) => {
|
||||||
|
|
||||||
const svgs = {
|
const svgs = {
|
||||||
"plus-circle": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
"plus-circle": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>,
|
</svg>,
|
||||||
"plus":<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
"plus":<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>,
|
</svg>,
|
||||||
|
"pencil": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||||
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={"h-10 w-10 " + className2}>
|
return <div className={"h-10 w-10 " + className2}>
|
||||||
|
|||||||
Reference in New Issue
Block a user