Skip to content

Instantly share code, notes, and snippets.

@ivanvinicius
Created August 3, 2023 23:26
Show Gist options
  • Save ivanvinicius/7d2e2cf60abb7e37e71823fa53592b69 to your computer and use it in GitHub Desktop.
Save ivanvinicius/7d2e2cf60abb7e37e71823fa53592b69 to your computer and use it in GitHub Desktop.
Check out the example
import * as RadixDialog from '@radix-ui/react-dialog'
import { ChangeEvent, useCallback, useState } from 'react'
import Cropper, { Area } from 'react-easy-crop'
import { useFormContext } from 'react-hook-form'
import { toast } from 'react-hot-toast'
import { RiCloseLine } from 'react-icons/ri'
import { Slider } from '~/components/Slider'
import { getBlobFromCroppedImage } from './canvas'
import {
InputImageContainer,
RadixDialogOverlay,
RadixDialogContent,
DialogCloseButton,
DialogCropperWrapper,
} from './styles'
interface Props {
name: string
}
type Stage = 'empty' | 'beforeCrop' | 'afterCrop'
export function InputImage({ name }: Props) {
const [stage, setStage] = useState<Stage>('empty')
const [openDialog, setOpenDialog] = useState(false)
const [imageURL, setImageURL] = useState<string | null>(null)
const [croppedImageURL, setCroppedImageURL] = useState<string | null>(null)
const [crop, setCrop] = useState({ x: 0, y: 0 })
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
const [zoom, setZoom] = useState(1)
const formCtx = useFormContext()
function reset() {
setOpenDialog(false)
setImageURL(null)
setCrop({ x: 0, y: 0 })
setCroppedAreaPixels(null)
setZoom(1)
}
function handleCloseDialog() {
if (stage !== 'afterCrop') {
setStage('empty')
formCtx.setValue(name, undefined)
}
reset()
}
function handleRemoveCroppedImage() {
setStage('empty')
formCtx.setValue(name, undefined)
setCroppedImageURL(null)
}
function readImage(image: File): Promise<string | ArrayBuffer | null> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.addEventListener('load', () => resolve(reader.result), false)
reader.readAsDataURL(image)
})
}
async function onFileChange(e: ChangeEvent<HTMLInputElement>) {
if (e.target.files && e.target.files.length > 0) {
const image = e.target.files[0]
const getImageURL = await readImage(image)
if (typeof getImageURL === 'string') {
setImageURL(getImageURL)
setStage('beforeCrop')
setOpenDialog(true)
} else {
toast.error('Houve um erro ao ler a imagem')
}
}
}
const onCropCompleted = useCallback(
(_croppedArea: Area, croppedAreaPixels: Area) => {
setCroppedAreaPixels(croppedAreaPixels)
},
[],
)
const handleCropImage = useCallback(async () => {
if (!imageURL || !croppedAreaPixels) {
toast.error('Houve um erro ao recortar imagem')
return
}
try {
const croppedImage = await getBlobFromCroppedImage({
imageURL,
croppedAreaPixels,
})
if (croppedImage) {
const croppedImageFile = new File([croppedImage.blob], 'filename', {
type: 'image/png',
})
formCtx.setValue(name, croppedImageFile)
setCroppedImageURL(croppedImage.url)
setStage('afterCrop')
}
} catch {
toast.error('Houve um erro ao gerar imagem recortada')
}
reset()
}, [imageURL, croppedAreaPixels, formCtx, name])
return (
<InputImageContainer>
<input
{...formCtx.register(name)}
onChange={onFileChange}
accept="image/*"
type="file"
multiple={false}
/>
{stage === 'afterCrop' && croppedImageURL && (
<div>
<img src={croppedImageURL} alt="" width={400} height={300} />
<button type="button" onClick={handleRemoveCroppedImage}>
remover imagem
</button>
</div>
)}
{stage === 'beforeCrop' && imageURL && (
<RadixDialog.Root
open={openDialog}
onOpenChange={handleCloseDialog}
defaultOpen={false}
>
<RadixDialog.Portal>
<RadixDialogOverlay />
<RadixDialogContent>
<RadixDialog.Close asChild aria-label="Close">
<DialogCloseButton type="button" title="Fechar modal">
<RiCloseLine />
</DialogCloseButton>
</RadixDialog.Close>
<DialogCropperWrapper>
<Cropper
image={imageURL}
zoom={zoom}
onZoomChange={setZoom}
cropSize={{ width: 720, height: 480 }}
cropShape="rect"
crop={crop}
onCropChange={setCrop}
onCropComplete={onCropCompleted}
/>
</DialogCropperWrapper>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Slider
aria-labelledby="Zoom"
min={1}
max={3}
step={0.1}
value={[zoom]}
onValueChange={([zoom]) => setZoom(zoom)}
/>
<button type="button" onClick={handleCropImage}>
Recortar imagem
</button>
</div>
</RadixDialogContent>
</RadixDialog.Portal>
</RadixDialog.Root>
)}
</InputImageContainer>
)
}
import * as RadixDialog from '@radix-ui/react-dialog'
import { keyframes } from '@stitches/react'
import { styled } from '~/styles/theme'
const overlayShow = keyframes({
'0%': { opacity: 0 },
'100%': { opacity: 1 },
})
const contentShow = keyframes({
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
})
export const RadixDialogOverlay = styled(RadixDialog.Overlay, {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.8)',
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
})
export const RadixDialogContent = styled(RadixDialog.Content, {
display: 'flex',
flexDirection: 'column',
gap: '$16',
padding: '$16',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '$8',
backgroundColor: '$white',
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
'&:focus': {
outline: 'none',
},
})
export const DialogCloseButton = styled('button', {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
background: 'transparent',
alignSelf: 'flex-end',
svg: {
fontSize: '$24',
color: '$gray300',
transition: 'color .2s',
'&:hover': {
color: '$red500',
},
},
})
export const DialogCropperWrapper = styled('div', {
position: 'relative',
width: '1280px',
height: '768px',
display: 'flex',
})
export const InputImageContainer = styled('div', {
flex: 1,
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment