Last active
June 8, 2023 02:04
-
-
Save samselikoff/07ada616bf1111f2b10b36241bc8a82b to your computer and use it in GitHub Desktop.
Diff of the PictureForm component from "Let's build a feature – Cropped Image Uploads!" https://youtu.be/W5__zfYrtt8
This file contains hidden or 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 Button from "@/components/Button"; | |
import ImageCropper, { | |
getCroppedImage, | |
getDataURLFromFile, | |
} from "@/components/ImageCropper"; | |
import Modal from "@/components/Modal"; | |
import useCurrentUser from "@/hooks/use-current-user"; | |
import useMutation from "@/hooks/use-mutation"; | |
import { UserIcon } from "@heroicons/react/outline"; | |
import { gql } from "graphql-request"; | |
import { useS3Upload } from "next-s3-upload"; | |
import { useState } from "react"; | |
export default function PictureForm({ onClose }) { | |
let { FileInput, openFileDialog, uploadToS3 } = useS3Upload(); | |
let [file, setFile] = useState(); | |
let [fileDataURL, setFileDataURL] = useState(); | |
let [crop, setCrop] = useState(); | |
let [updateCurrentUser] = useMutation(updateCurrentUserMutation); | |
let currentUser = useCurrentUser(); | |
let [isSaving, setIsSaving] = useState(false); | |
async function handleFileChange(file) { | |
let dataUrl = await getDataURLFromFile(file); | |
setFile(file); | |
setFileDataURL(dataUrl); | |
} | |
async function handleSavePhoto() { | |
setIsSaving(true); | |
let croppedPicture = await getCroppedImage({ | |
dataURL: fileDataURL, | |
crop, | |
fileName: file.name, | |
aspectRatio: 1, | |
}); | |
let { url } = await uploadToS3(croppedPicture); | |
await updateCurrentUser({ currentUserId: currentUser.id, avatarUrl: url }); | |
onClose(); | |
} | |
return ( | |
<Modal onClose={onClose}> | |
{/* Header */} | |
<div className="px-4 py-5"> | |
<div className="relative"> | |
<button | |
type="button" | |
className="absolute text-sky-500" | |
onClick={onClose} | |
> | |
Cancel | |
</button> | |
<Modal.Title className="font-semibold text-center text-gray-900"> | |
Edit picture | |
</Modal.Title> | |
<div className="absolute inset-y-0 right-0"> | |
<Button | |
isSaving={isSaving} | |
onClick={handleSavePhoto} | |
type="submit" | |
disabled={!file} | |
className={`${ | |
file ? "text-sky-500" : "text-gray-400" | |
} font-medium`} | |
> | |
Save | |
</Button> | |
</div> | |
</div> | |
</div> | |
<FileInput onChange={handleFileChange} /> | |
{/* Main */} | |
<div className="p-3"> | |
{!fileDataURL ? ( | |
<div className="relative rounded-full aspect-1"> | |
<button | |
onClick={openFileDialog} | |
className="absolute inset-0 flex flex-col items-center justify-center border-2 border-gray-300 border-dashed rounded-full" | |
> | |
<UserIcon className="w-24 h-24 text-gray-200" /> | |
<p className="mt-2 text-sm font-medium text-sky-500"> | |
Choose photo | |
</p> | |
</button> | |
</div> | |
) : ( | |
<div> | |
<div className="relative z-0 overflow-hidden rounded-full"> | |
<ImageCropper | |
src={fileDataURL} | |
aspectRatio={1} | |
crop={crop} | |
onCropChange={(crop) => setCrop(crop)} | |
/> | |
</div> | |
<div className="text-center"> | |
<button | |
onClick={openFileDialog} | |
className="px-2 py-1 mt-4 text-sm font-medium rounded text-sky-500 focus:outline-none" | |
> | |
Replace | |
</button> | |
</div> | |
</div> | |
)} | |
</div> | |
</Modal> | |
); | |
} | |
let updateCurrentUserMutation = gql` | |
mutation ($currentUserId: String!, $avatarUrl: String!) { | |
update_users_by_pk( | |
pk_columns: { id: $currentUserId } | |
_set: { avatarUrl: $avatarUrl } | |
) { | |
id | |
} | |
} | |
`; |
This file contains hidden or 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
/** | |
* @param {HTMLImageElement} image - Image File Object | |
* @param {String} dataURL - | |
* @param {Object} crop - crop Object | |
* @param {String} fileName - Name of the returned file in Promise | |
* @param {Number} aspectRatio - | |
*/ | |
export async function getCroppedImage({ | |
image, | |
dataURL, | |
crop, | |
fileName, | |
aspectRatio, | |
}) { | |
let _image; | |
if (image) { | |
_image = image; | |
} else if (dataURL) { | |
let image = new window.Image(); | |
image.src = dataURL; | |
_image = image; | |
} else { | |
throw new Error("Pass an image or a dataURL"); | |
} | |
let { naturalWidth, naturalHeight } = _image; | |
let imageAspectRatio = naturalWidth / naturalHeight; | |
let imageIsWiderThanContainer = imageAspectRatio > aspectRatio; | |
let width = imageIsWiderThanContainer | |
? aspectRatio * naturalHeight | |
: naturalWidth; | |
let height = imageIsWiderThanContainer | |
? naturalHeight | |
: (1 / aspectRatio) * naturalWidth; | |
const canvas = document.createElement("canvas"); | |
const scaleX = crop.scale; | |
const scaleY = crop.scale; | |
canvas.width = width / scaleX; | |
canvas.height = height / scaleY; | |
const ctx = canvas.getContext("2d"); | |
ctx.drawImage(_image, crop.x, crop.y, width, height, 0, 0, width, height); | |
// As Base64 string | |
// const base64Image = canvas.toDataURL('image/jpeg'); | |
// As a blob | |
return new Promise((resolve) => { | |
canvas.toBlob( | |
(blob) => { | |
blob.name = fileName; | |
resolve(blob); | |
}, | |
"image/jpeg", | |
1 | |
); | |
}); | |
} | |
export async function getDataURLFromFile(file) { | |
let reader = new FileReader(); | |
return await new Promise((resolve) => { | |
reader.addEventListener( | |
"load", | |
() => { | |
resolve(reader.result); | |
}, | |
false | |
); | |
reader.readAsDataURL(file); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment