I have an application that allows the user to upload photos, to edit them (crop, zoom and rotate) and to download.
I'm trying to add a preview mode, so the user can see how the file he's downloading will look like.
What I did works fine except for one thing - when the user clicks on the Clear Preview button the photos go back to their original state (the state is being reset on Clear Preview button click),
without keeping the changes the user made to them.
Any idea what am I missing and how to prevent the state reset?
export default function ImagesGrid() {
const classes = useStyles({})
const {
photos,
preparePhotos,
checkUnusedSuppliedTags,
usedTagsList,
suppliedTags
} = useContext(SitePhotosContext)
const taggedPhotos = photos.filter(photo => photo.tags.length)
const pages = Math.ceil(taggedPhotos.length / 6)
const [isPreviewMode, setIsPreviewMode] = useState(false)
const [preparedPhotos, setPreparedPhotos] = useState([])
const renderedPhotos = isPreviewMode ? preparedPhotos : photos
let countColor: TypographyProps['color'] = 'initial'
const tags = usedTagsList.concat(
checkUnusedSuppliedTags(usedTagsList, suppliedTags)
)
useEffect(() => {
preparePhotos(photos).then(photos => setPreparedPhotos(photos))
}, [photos])
return (
<div className={classes.root}>
<AppBar position="sticky" color="default">
<Toolbar>
{!isPreviewMode ? (
<div className={classes.toolbarText}>
<Typography
variant="h6"
color={countColor}
className={classes.pageCount}
>
{`${taggedPhotos.length} Tagged Photos / ${pages} Pages`}
</Typography>
{taggedPhotos.length === 0 && (
<Typography
variant="body2"
color={countColor}
className={classes.instructions}
>
Tag some photos to generate a report
</Typography>
)}
</div>
) : (
<DownloadPreflightModal
photos={preparedPhotos}
usedTagsList={usedTagsList}
/>
)}
<DownloadAndPreview
isPreviewMode={isPreviewMode}
setIsPreviewMode={setIsPreviewMode}
tags={tags}
/>
</Toolbar>
</AppBar>
<Grid
container
justify="flex-start"
spacing={2}
className={classes.imageGrid}
>
{renderedPhotos.map(photo => (
<Grid
item
xs={12}
sm={6}
md={isPreviewMode ? 6 : 4}
key={photo.id}
className={isPreviewMode ? classes.imageInternalGrid : ''}
>
<Image photo={photo} mode={isPreviewMode ? 'preview' : 'editor'} />
</Grid>
))}
</Grid>
</div>
)
}
DownloadAndPreview component:
export default function DownloadAndPreview({
isPreviewMode,
setIsPreviewMode,
tags
}) {
const classes = useStyles({})
const generatorRef = useRef()
const {photos} = useContext(SitePhotosContext)
return (
<>
{!isPreviewMode ? (
<Button color="inherit" onClick={() => setIsPreviewMode(true)}>
Preview
<VisibilityIcon />
</Button>
) : (
<div>
<Button
color="inherit"
onClick={() => setIsPreviewMode(false)}
className={classes.previewModeBtn}
>
Clear Preview
<ClearIcon />
</Button>
<Button
color="inherit"
onClick={() => {
downloadDoc(generatorRef, photos, tags)
}}
>
Download
</Button>
</div>
)}
</>
)
}
<Image /> component:
export default function Image({photo, mode}: {photo: ImageItem; mode: string}) {
const classes = useStyles({})
const {
setImageEditData
} = useContext(SitePhotosContext)
return mode === 'editor' ? (
<Paper>
{!photo.src && (
<div className={classes.loading}>Loading {photo.data?.name}...</div>
)}
<div>
<ImageEditor
photo={photo}
onEditComplete={editData => {
setImageEditData(photo.id, editData)
}}
/>
</div>
</Paper>
) : (
<div>
<img src={photo.src} className={classes.img} />
<label>{photo.tags.join(' / ')}</label>
</div>
)
}
<ImageEditor /> component:
export default function ImageEditor({
photo,
onEditComplete,
showControls
}: editorProps) {
const classes = useStyles({})
const [crop, setCrop] = useState({x: 0, y: 0})
const [rotation, setRotation] = useState(0)
const [zoom, setZoom] = useState(1)
const onCropComplete = useCallback(
(croppedArea, croppedAreaPixels) => {
const editSettings: editData = {crop: croppedAreaPixels, rotate: rotation}
onEditComplete(editSettings)
},
[rotation, onEditComplete]
)
return (
<>
<Box className={classes.cropContainer}>
<Cropper
image={photo.src}
aspect={maxImageWidth / maxImageHeight}
crop={crop}
rotation={rotation}
zoom={zoom}
zoomWithScroll={false}
onCropChange={setCrop}
onRotationChange={setRotation}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
</Box>
{showControls && (
<Box display="flex" className={classes.controls}>
<IconButton
color="primary"
onClick={() => setRotation(rotation => rotation - 90)}
>
<RotateLeftIcon />
</IconButton>
<Box display="inline-flex" width="100%">
<ImageIcon
fontSize="small"
color="action"
className={classes.imageIcon}
/>
<Slider
className={classes.slider}
value={zoom}
min={1}
max={3}
step={0.1}
onChange={(e, zoom) => setZoom(zoom)}
/>
<ImageIcon
fontSize="large"
color="action"
className={classes.imageIcon}
/>
</Box>
<IconButton
color="primary"
onClick={() => setRotation(rotation => rotation + 90)}
>
<RotateRightIcon />
</IconButton>
</Box>
)}
</>
)
}
context functions:
const preparePhotos = async (photos: PhotoList) => {
const res = photos
.filter(photo => photo.tags.length)
.sort(sortByPriority)
if (res.length % 2) res.pop()
const edittedPhotos = await Promise.all(res.map(editImage))
return edittedPhotos
}
const setImageEditData = (id, editData) => {
setPhotos(photos =>
photos.map(photo => {
if (photo.id === id) {
return {...photo, editData}
}
return photo
})
)
}
const editImage = async (image: ImageItem) => {
if (!image.editData) return image
const {crop, rotate} = image.editData
const edittedImage = await cropAndRotateImage(image.src, crop, rotate)
return {
...image,
src: edittedImage,
dimensions: {width: crop.width, height: crop.height}
}
}
Expected behavior is similar to this: https://codesandbox.io/s/react-easy-crop-custom-image-demo-y09komm059?from-embed=&file=/src/index.js:3141-3151
After the user closes the preview modal the photo stay in the same position it was - before clicking on the preview button. (What by me - it doesn't)
Your question still lacks some vital information. But let me guess...
<Cropper> calls onCropComplete. And it rerenders every time you switch into editor-mode.react-easy-crop sets crop, rotation and zoom to respective values passed to <Cropper> which you hold in <ImageEditor> component's state.function Image(...) {
...
return mode === 'editor' ? ( ... <ImageEditor ... /> ... ) : ( ... )
}
you loose <ImageEditor> component's state on every mode switch.preview-mode into editor-mode <Cropper> receives default values for crop, rotation and zoom, calls onCropComplete and your implementation of onCropComplete sets editData to these default values.There are several ways to fix this. But the main issue, as I see it, is that you don't have a single source of truth for photo's editData. So I think you should get rid of <ImageEditor> component's state and instead pass editData to the <Cropper>. Like so:
<Cropper
image = {photo.src}
crop = {photo.editData.crop}
rotation = {photo.editData.rotation}
zoom = {photo.editData.zoom}
...
/>
You don't have a minimal reproducible example, so I can't test it. For this to works you should probably at least add some default values for photo.editData
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With