WIP #36
@@ -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