Events are exciting in nature, but a wall of text in an app… isn’t the most exciting. We can liven this up by using images within our application, particularly allowing our event submitters to attach a photo that represents that particular event whether it's an actual photo or something that looks along the lines of what to expect.
But this isn’t as simple as storing a string of text in a Database. Images and files generally are complex and need more support to handle them, which is where the Appwrite Storage service comes in.
We’ll first learn how we can read a file from a form’s filepicker input and read the information about an image like the width and height, which will help us to display that image confidently later on. We’ll then see how we can spin up a new Storage Bucket, start to upload our files, store a reference to that image in the related event Document, and even display the images once permissions are configured.
What you’ll Learn
Instructor: [0:00] Now that we have a lot of our event data managed inside of our database, we're going to start to explore a different API. That's going to be the storage API. We're going to see how we can upload a file, and in particular, an image to represent each of our events.
[0:13] Back inside of our application, anytime we want to add a new event, we want to make sure that if we select a file, we can actually upload this file and associate it with that event.
[0:22] If we head into our pages, events, new file, we know from previous lessons that we have this handle on submit function that every time the form is submitted, will fire this function, will grab the data, and will actually use it to create an event. Currently, we're not grabbing that file associated with the picker.
[0:38] Now, because of the way that file inputs actually work, where this particular file input is just simply a wrapper around the file picker input itself, so there's nothing really special going on here. If we try to grab the value from this similar to how we're currently doing it from the other inputs, it's not going to work as expected.
[0:55] What we actually need to do is listen for this file picker input to change and then grab the value from that event. What we're going to do is to start off by adding an onChange handler, where let's call this function handle on change.
[1:09] We're going to define this new function up above my onSubmit handler, where inside I'm going to do something similar to inside that submit handler, where I'm going to define my target.
[1:19] To start, I need to actually define my event as one of the arguments where this is going to be a react.form event, where inside I'm going to specify HTML input element. Now, I'm going to say constant target is equal to event.target, but I'm going to specify that as HTML input element and my custom object, which includes files, which will be a file list.
[1:45] When this runs, I'm going to grab this list of files from my target element that I can actually use to upload to the server. First, let's just test out to see what that's actually going to give us. I'm going to log out target.files, where I'm going to actually log that out as well. If I head to my form, I can see when I select a file, I actually get that file list that includes that single file.
[2:06] Now, technically, I could probably take target.files, store it into state and then submit it with the event handler. What that's not giving us are things like width and height, which are important for being able to display those images properly inside of the application.
[2:20] We're going to set up a constant of image, and we're going to set that equal to new image. We're going to say image.source is equal to URL.create object URL, where we're going to pass in target.files, where I'm only going to grab the first one.
[2:35] Ultimately, we only want one file associated with each event. Now technically, you can set up that file picker to accept multiple images. For now, we're just going to focus on getting this single file. Now once this loads, this is going to be able to give us more information about this image.
[2:49] We need to be able to capture that. We're going to say, "image.onload." We're going to set that equal to a new function, where inside we're going to be able to have access to that image data. We want to ultimately capture that along with the file so that we can pass it to our server. Before we go any further, let's actually set up our state instance.
[3:05] Const image and set image is equal to useStates. For now, I need to define the type for this image. I'm going to say, "interface lye B image." We're going to have a few things, including the width, which is going to be a number. We're going to also have the height. Finally, we're going to have a file, which is going to be the file data, which will just simply be an actual file.
[3:27] Then we're going to type it out on our state where we're going to be able to pass that in as our lye B image. Now let's go back to that onload function, where we're going to say, "set image." Inside, we want to define those different parameters, including our width, our heights, and our file. This file is going to be our target.files, where we're going to grab that first one.
[3:49] Our height is going to be image.height. Our width is going to be image.width. Let's console-log this out just to make sure that it's working properly. We can see it's undefined to start. Once we select our file, we can see that we do get that meta information of our width and our height along with the actual file.
[4:06] Inside of our submit handler, we can look for that file and make sure that it's there. If it is there, pass it along with the create-event function. We can't actually store the image itself on this event. We need to store a reference to that image. We're going to use the Appwrite Storage in order to upload that file, get an ID for that file, and then store that ID as the reference on the event.
[4:29] Heading over to the Appwrite Storage API docs, we're going to particularly use the create file endpoint, where the API should look pretty similar to some of the other work we've done so far in this course. We're going to have a few different pieces of information that we're going to need to pass in.
[4:42] If we look at this particular instance, we can see that we have a bucket ID and we have a file ID, which we can imagine that's going to be an ID.unique value. Then we can pass in the file itself. To do this, it sounds like we do need to have a bucket ID. Let's first start off by creating our new bucket. Inside of the Appwrite dashboard, I'm going to head over to the storage tab.
[5:02] I'm going to go ahead and create a new bucket. I'm going to simply call that "images" and then click "create." Inside of this bucket, this is where I'm going to store my files. They don't need to just be images. They can really be whatever they want. For our purposes, they're going to be images. We can even test this out to see what it looks like by adding a new file manually.
[5:18] If I click "create file" and I click "choose file," I can upload one of my images. We can see that it gives me some information about permissions, which we'll get to in a second. I click Create. Once it's done uploading, we can see that I have my new file.
[5:30] If I go to it, we can see the information associated with that, we can even see that I can copy the file URL. If I go to that, we can see that our new image loads. We currently loaded that file inside of the browser of our current Appwrite session.
[5:43] If we load that image inside of an incognito tab for instance, or if we're not logged in, we can see that we can't access that because we haven't associated any permissions with this. If we head back to our bucket, similar to what we did with our database earlier, and go to settings, we can scroll down. I'm going to exit out of uploading files.
[6:01] We can see that we have update permissions, so I'm going to add a role. For now, I'm just going to say, I want anyone to be able to access any of these files, because ultimately, I want anybody to be able to read this file and be able to see it on the Livebeat Events site, whether or not they're logged in.
[6:17] I'm going to hit update, and next time I try to access that file, we can see that it loads inside of my incognito window. Ultimately, we don't want to just upload these directly inside of the admin UI, we want to do it programmatically inside of our project. Let's get to that.
[6:30] To start off, I want to import the storage API from Appwrite, where I'm going to create a new instance of storage and use that create file endpoint. As we should expect from previous lessons, let's start off inside a Lib Appwrite where I'm going to import storage. With storage, I'm going to export a new constant of storage with a new instance of storage.
[6:50] Additionally, I don't want to interface directly with that storage instance, I want to do something similar to what I did with events.
[6:57] What I'm going to do is create a new file called storage.ts, where inside I'm going to import storage from at Lib Appwrite, where I'm going to export a new async function called uploadFile, where here I ultimately want to be able to take a file, which will have a type of capital file, and I want to use this storage API to upload that.
[7:18] Let's say constant data is equal to awaitstorage.create file, where our parameters are going to be bucket ID, our unique ID, and then finally the file itself.
[7:30] Starting with the bucket ID, let's set that up as an environment variable, where let's duplicate one of these lines where I'm going to call it events bucket images ID, where inside of Appwrite, I can copy that bucket ID right by the title of my bucket itself, and of course, update it inside of my env file.
[7:46] Now, I can replace this bucket ID with import.meta.env. that bucket ID environment variable. Next, for the unique ID, we learned in past lessons that we can use the ID helper from Appwrite. We're going to do the exact same thing. I'm going to import ID from the Appwrite package. Remember to go back to create file, and I'm going to replace that with ID.unique.
[8:09] Again, we already added our file as that third parameter, which is going to be the data that we upload to storage, but now we can simply return that data back to the caller. Back on the new event page, we need to first start by importing that function. I'm going to import uploadFile and update that to import from storage.
[8:27] For inside of the submitHandler, what I want to happen is I'm going to check and see if that file exists, and if that file does exist, I'm going to pass that reference along to the create event. We need to create that reference inside of our database, but for now, let's start to get it and upload the file.
[8:43] I'm going to say let file, and I'm going to check to see if image file exists. If it does exist, I'm going to say file is equal to await upload file, where I'm going to pass in that image.file. You know me, I like to test everything out as I go along.
[8:59] Let's log out our file just to see what that looks like. I'm also going to add a return statement here, just so that we only upload that file while we're testing this. If I go ahead and choose one of my files and I submit it, we can see that, oh, we have an error. That's because we never gave permissions to create new files, we only gave permissions to read files.
[9:20] Let's go back to our settings. We know that we need to make sure that for now anybody can create a file. As I mentioned in previous lessons, we'll later come back to see how we can set up granular permissions for logged-in users and even admins.
[9:33] Let's update those permissions, and I'm going to try to submit that request where after it's uploaded, we can see that pop into our console where we have all that new information about our new file that we uploaded.
[9:44] We can even confirm that we do now see that inside of our bucket, but back looking at this metadata, we can see that there's a lot of information in here. What we particularly want to use is this ID, where we're going to store this ID as a reference in our database to associate it with the event that we're creating.
[10:01] First, we need to set up that reference. Let's head over to databases, or if we go to events, events, and then to attributes, we know that we currently have our name, location and date, but now we need some attributes associated with our image.
[10:15] Now looking at the type we set up for Lib image, we know that we want to store the width and the height, but then we want to also store that file ID reference that we were talking about.
[10:23] Let's start off by creating our new attribute, which let's call it image width, which if we select the type is going to be an integer, where we don't really need to define any of these other things, but we can Click Create.
[10:33] We know that we also need our image height, which will also be an integer. We also need that. We need to store the image file ID, and we can select a type of string for that one. We could probably use something like 120 like the other fields that we used. If we look back inside of the docs, we can see that this file ID is going to be a max length characters of 36.
[10:53] We could probably just use 36 as our size. We don't need any of the other attributes filled out here, and we can Click Create. Now that we have all of our image attributes, let's start to pass this in. Back inside of our submit handler, we can now remove that console.log in a return statement.
[11:08] Inside of this createEvent function, I'm going to pass in my image width, my image height, and my image file ID. After alphabetically ordering these, we can know that we are going to get that image.height from the state as well as the image.width. For the file ID, we're going to get that from the file that we just uploaded.
[11:30] We're going to specify that as file.$ID. Now, because we don't know that we have selected an image, we need to make sure that for each of these, that we're also adding that optional chain to make sure that it exists first. Now we have one other problem, and it's that our createEvent doesn't know about these new fields.
[11:48] If we remember from previous lessons, this createEvent function that we're creating is associated with Lib event. The only thing that we're emitting is the ID. Pretty much we need all the information when we're creating this to be passed along with this event.
[12:02] Inside of our types events, we need to now define all those image fields where we can specify our image file ID, which will be a string. We also want to make sure that's optional because again, we don't need to submit an image along with it, but we should. That's optional. If you want to make it required, you certainly can.
[12:20] Then we also have the image height, which is a number and make that optional as well as the image width. I'm realizing I said this was an ABC order, but of course the F comes before the H. As we can see with that type update, we now have happy fields that we're passing into createEvent.
[12:36] Let's give this a shot. If I have my form ready to go and I select my image file, I'm going to go ahead and click submit, which we see was a success, so it redirected us over to the event page.
[12:46] If we look inside of our documents and scroll down and find that new one, we can scroll down into the data and we can now see that we have the width, the height and the file ID. This is great progress. We're now storing that file ID, so now we can look that up and use it inside of the application.
[13:01] If we head to the upright docs, we can see some of the APIs associated with being able to interface with our files. We can see that we can get a file, but that's just going to return the metadata of the file, but what we additionally have is this Get File for Preview as well as Get File for View.
[13:18] I'm going to check out this Get File Preview where if we start to look at this end point, what is going to allow us to do is be able to pass in that file ID and easily get the image data.
[13:26] We can see what this looks like with the SDK where we use this Get File Preview method where we pass in the bucket ID and the file ID, and this is going to give us the URL that we can use to display in our application.
[13:37] Before we start to use that, let's create a helper function like we do with most of the SDK interfacing we do. Let's export a new function. If we notice, we're not creating an async function, this is going to be a synchronous function that we're dealing with, so we don't need to make it async.
[13:51] We can say get preview image by ID where the argument is going to be our file ID, which we can set to a string where let's return storage.get file preview where the first argument for this is going to be our bucket that we can grab right from the above, which are bucket images ID. I'm going to pass that in. Then our second argument is going to be that file ID.
[14:15] Let's head over to pages where we're going to go to our event, event ID page, or if you remember, we commented out some of the stuff related to images because we currently didn't support it inside of our application. Let's start off by uncommenting out some of that image information, including where we're displaying the image.
[14:33] Now I'm going to first import my get preview image by ID from lib-storage. Where to start, I want to make sure that I have an event available before I try to look up this image. The first thing I need to do is move this image reference below my event. What I want to do is create a new constant called image URL, and I'm going to set that equal to get preview image by ID.
[14:58] First, I want to make sure that my event image file ID even exists first, where I would then pass in that file ID to my function, where I could then take the image URL and I can pass it in as that URL property for my image object.
[15:14] Let's do a progress check and see what happens now that we're defining that image URL because ultimately, that should be passing directly into that image tag. If I refresh the page, we can see that it's still not showing. If we start to debug by console logging out our event, and we inspect that data, we can see that we don't have that information there.
[15:31] Heading over to the Get Event by ID function, we remember that we created this map document to Event function because of the way that the document system was working, so that we can type this out and be able to grab that event data and pass it in through our functions.
[15:45] Now we need to create those new properties on this mapping function so that it gets passed back when we're requesting that data. Let's start off with our image height, which is going to be document.imageheight.
[15:57] Let's duplicate that line where it's going to be our width, and then we can also grab our file ID. Once the page reloads, we can see that image pop in because that data is now on our event object.
[16:10] We also want to make sure that we have our image width and our height, so we're going to set our height to event, optional chaining ImageHeight, as well as our width, which we're going to make sure we capitalize in the camel case, where then we can also specify image.width and image.height, where we can now see that we're passing in the actual width and the height into the application.
[16:33] Passing an image this size isn't optimal for an application as this is way larger of a file than we need. While we're not going to cover this, the Get File Preview method also includes some parameters that you can pass in, including width and height, which will dynamically resize it down only to the size that you need.
[16:49] All that's left is being able to show the images for all the events itself on the home page. Let's leave that as a challenge for you to be able to go through the application and try to update the files needed in order to show those images. If you get stuck, you can find the source for this lesson right inside of the description.
[17:04] At this point, we're now successfully supporting all the different fields that we want and need for event creation. What about being able to delete events such as these test events that I've created as we're working through this? Next up, we're going to set up the ability to delete an event by taking advantage of both the database and storage API and their ability to delete.