Created
August 3, 2023 23:26
-
-
Save ivanvinicius/7d2e2cf60abb7e37e71823fa53592b69 to your computer and use it in GitHub Desktop.
Check out the example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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