Skip to content

Instantly share code, notes, and snippets.

@samselikoff
Last active June 8, 2023 02:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samselikoff/07ada616bf1111f2b10b36241bc8a82b to your computer and use it in GitHub Desktop.
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
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
}
}
`;
/**
* @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