Places Diary #5: Check-ins and Instant Image Previews for Image Uploads
One of the key features of Places is the ability to keep track of places you’ve been. I like to remember what I’ve tried and how it was, so Places includes “Check-ins” to add notes or a photo anytime there is something I want to remember about a place. If a note and photo is omitted, the date and time of a visit is saved. A timeline of check-ins is visible on each place’s page and on the dashboard.
I’ve had the feature working for quite a while, but I wasn’t happy with the UI for adding a photo. Last weekend I finished rewriting the photo uploader so that an image preview appears instantly when an image is added to a check-in. Before, it would wait for the upload to complete before showing a preview, leaving me to wonder whether the upload was working at all when out and about or on a slow connection.
Under the hood, this uses the FileReader
API which lets web apps read the contents of a file as soon as it’s selected by the user instead of waiting for it to upload and getting information about the file from the server. Using the readAsDataURL()
method, the image is converted to a base64 string that can be displayed as in image in the browser while the image uploads.
The actual image upload only begins once the image preview is displayed (which essentially happens instantly) and a loading icon is displayed over the image preview until the upload is complete.
This is what the code looks:
const [media, setMedia] = React.useState([]);
const handleFileInputChange = async (event) => {
const files = event.target.files;
if (!files) {
return;
}
/** Create an image preview for each file */
for (const file of files) {
const reader = new FileReader();
reader.addEventListener(
"load",
() => {
/** Convert image file to base64 string */
const img = new Image();
img.src = reader.result as string;
/** This is called when the read operation is completed */
img.onload = async () => {
/** Add the image that will be uploaded to the media state to display while the image is being uploaded */
setMedia((prevMedia) => [
...prevMedia,
{
height: img.naturalHeight,
width: img.naturalWidth,
src: img.src,
filename: file.name,
/** Indicates the image is being uploaded so a spinner can be shown */
state: "uploading",
},
]);
/** Upload the image */
const formData = new FormData();
formData.append("file", file);
const upload = await fetch("/media", {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => {
setMedia((prevMedia) => {
return prevMedia.map((media) => {
/** Replace the optimistic media with the uploaded media */
if (media.filename === data.originalFilename) {
return {
height: data.height,
width: data.width,
src: `${process.env.ASSETS_URL}/${data.filename}?h=126&dpr=2`,
filename: data.originalFilename,
};
}
return media;
});
});
return data;
});
};
},
false
);
reader.readAsDataURL(file);
}
};
/** Show the images */
{media.map((media, index) => (
<div className={`relative h-32`} key={index}>
<img
height={media.height}
width={media.width}
src={media.src}
className={`h-full max-h-full w-auto ${
media.state === "uploading" ? `opacity-50` : ``
}`}
/>
{/** Show a spinner for media that is still uploading */}
{media.state === "uploading" ? <Spinner /> : null}
</div>
))}
Interested in trying Places?
If you are interested in trying Places, sign up for the waitlist and be among the first to know when it’s ready.
Other posts in this series
All Posts