Welcome to Part 4 of creating a twitter clone with React and Supabase! If you follow along, by the end of the series, you will have deployed a fully functioning app that lets users:
- tweet out what they are thinking,
- upload avatars and change their profile,
- be notified when there are new tweets, and;
- be notified when someone has liked their tweet.
In Part 3, we made things a bit prettier, added a nav bar and an edit profile page.
In Part 4, we will:
- finish off the edit profile page, letting users change their username and upload an avatar to the Supabase storage.
- create a
tweets
andfavorites
table in Supabase for the users' tweets and display them usingreact-query
library.
The changes for this part is in this commit. Sorry its a big commit 🙏 but I'll be including important snippets below. In this section, we'll be using Supabase's handy storage to let users upload their avatar to our website.
- We need to create a bucket in Supabase
- We need to let users upload the image to the bucket and save the filename to our profiles table.
- We need to be able to fetch the image from the bucket and display it using the saved filename.
If you've been following this guide, you've already done this in Part 1, when we used Supabase's user management starter SQL ! If you haven't, you can just run the below snippet in the SQL query editor.
https://gist.github.com/8573ae6a8ebadcc0b81c02a6092d69a6
You can also easily do this via the UI, by going to the storage section and clicking the 'New bucket' button.
![[p4 - storage ui.png]]
Letting the users upload a file to Supabase is super easy. All you need do is: https://gist.github.com/ae02a6beb97dd80c35094c41d3719a5e
The filename can be anything you want. The file to be uploaded can be chosen via user's file browser by using theinput
element with type of file
. In the git commit, I've customised the input button to look a bit different by putting it inside a Button
component.
https://gist.github.com/21cfba8f886ba20f7fe9d4d16acc293a
This looks something like this.
![[p4 - upload avatar.png]]
The onChange
callback gets called with the ChangeEvent<HTMLInputElement>
whenever the user selects an image. So I've chosen to upload and display the file whenever user selects an image. This logic is extracted out into a hook, useUpload
. In the hook, I get the file with event.target.files[0]
then send it off to Supabase.
https://gist.github.com/fa2bb660462862f78f4a81360879f639
Key
's value is the resulting filename, prefixed with the bucket name. e.g. avatars/my-image.png
. I save the value of Key
as avatar_url
in the profiles
table when the user submits the form.
There are two ways to show our image to the user.
One way is to give a presigned url to the user. https://gist.github.com/99a0e7ad7dc0b701eecf0fd976d6155e
Another way is to give download the object as a blob, then creating a URL from the blob. I chose this way in my project. https://gist.github.com/0b75e51bc96e4ceefb2fe211b199898e
When I started writing the blog post, there was a bug with supabase where it wouldn't set the Cache-Control
headers for S3 objects and defaulting to no-cache
value. This meant that big images were not being cached by the browser at all.
![[p4 - multiple tweets.png]]
I've raised a ticket when I've discovered it and this has been fixed as of 1.11.14!
When the bug hadn't been fixed yet, I've used react-query's caching feature to cache the image in the app instead. The follow snippet will cache and return whatever the promise (fetchAvatar
) have returned for an hour (set via staleTime
), if the path
matches. If you are interested, you can read more about caching via keys in react-query's docs.
https://gist.github.com/0663c107ec15c9ecd0ad7e89be515fe4
As of Supabase version 1.11.14, the browser will respect the cache it for us as Cache-Control
will be set properly (by default, it will be max-age=3600
).
With profiles working, its time for us to move to the main event: tweets! The changes for this part is in this commit.
I've kept tweets table quite simple. A tweet will has the following fields:
id
,Big int, Primary Key (auto-generated)createdAt
, timestampz (auto-generated)content
, textuserId
, uuid, Foreign Key forprofiles
table
I've created a favorites table. A favorite has the following fields
id
, Big int, Primary Key (auto-generated)inserted_at
, timestampz (auto-generated)tweetId
, Big int, Foreign Key fortweets
tableuserId
, uuid, Foreign Key forprofiles
table
![[p4 - db relations.png]] made with dbdiagram
I don't have SQL queries because I've made both tables with the UI 😅. I've added some test data via UI too. We now need a way to query the db for display out tweets. The query will get a bit complicated, but Supabase can handle that 💪 with functions.
Let's have a look at a tweet see and what data the front-end needs. ![[p4-tweet.png]]
A tweet needs:
- the user's avatar
- the user's name
- when the tweet was created
- the tweet's content
- how many people favorited the tweet
- (if logged in) whether the user has favorited it
This might look this as a Typescript type https://gist.github.com/f5eec6c4a409ba915577a1844973f248
Getting the tweet author's profile is simple enough, it's just a JOIN
. But how do we get an array of users who has favorited the tweet? I've booted up supabase's SQL query editor and started crafting a query. With some help from the Stack Overflow, I got a query working.
https://gist.github.com/a47a31f2ebd8cfe0ad8bbad5f067ad0b
Postgres functions like json_agg
and json_build_object
are available in Supabase, so using those, the above query gives us a list of tweets with
- id
- content
- createdAt
- favorited_users, an array of favorited users in JSON
- tweet_author, the user who wrote the tweet in JSON
![[p4 - query result.png]]
Now that we have a query that works, we need a way to call this query from our React app. Conveniently for us, Supabase lets you call posgres functions from front-end, so let's go make a function.
We now need to wrap the above query in a function. I've added u_id
optional parameter to the function. When this is supplied, it will filter the tweets by the user's id. This will be used to display only the user's tweets when their profile page.
https://gist.github.com/5d33c435b124748cac26b22fa6f37cec
Once you successfully create the function, you can see that the docs for our function are automatically generated in the "API" section, under "stored procedures".
![[p4-api-stored-procedures.png]]
Calling these are as simple as https://gist.github.com/524c81956f0055fea3aac51dd93d585b
In my commit, I do a ilttle bit of transformation (like figuring out whether the current user has favorited the tweet) to the response to get the data we need for the front-end . https://gist.github.com/195f9940501be4150e12045f8dcee201
This result is then given to TweetCard
component using react-query
.
https://gist.github.com/7aef112f62d6060355ed526e187fed8d
Resulting in a list of tweets!
![[p4 - tweet results.png]]