Table of contents:
- Demo
- Overview
- Setting up Firebase
- Post Upload
- Progress Bar
- Post Download
- Image Compression
Demo
You can check on the full source code and try them in Replit.
Repl url: https://replit.com/@arnoldschan/PostUpload
Overview
User flow
- As a user, they can upload a post by:
- Choose a picture
- Fill in the caption
- Hit the upload button
- The progress bar shows the uploading process.
- The new uploaded post will show in the timeline
- User can see all of the uploaded posts
File tree:
This is how the project file tree looks like.
- The main
App.jsx
is in root folder - The smaller components in
components/
folder - Components' css in
css/
folder - Anything related to firebase is inside
firebase/
folder
Setting up Firebase
You can follow the setting up firebase project guidelines here. After you have set up the project, initiate firebase modules in firebase.js
:
//firebase.js
import firebase from 'firebase';
import firebaseConfig from "./firebaseConfig";
const firebaseApp = firebase.initializeApp(
firebaseConfig
)
const db = firebaseApp.firestore();
const storage = firebaseApp.storage();
export { db, storage }
The Firestore and Firebase Storage can be used directly without configuring anything in Firebase portal. You may need to change the security rules to open the database for the public (unless you want to implement authentication).
Post Upload
Choose a picture
//PostUpload.jsx
const [file, setFile] = useState(null)
//...
const chooseFile = (e) => {
if (e.target.files[0]){
setFile(e.target.files[0]);
}
}
//...
<Input
id="fileinput"
className="child"
type="file"
name="upload-file"
onChange={chooseFile}
/>
The user interacts with <Input/>
element to open up the file explorer pop-up. If the user chooses a file, chooseFile
function will be triggered.
In this function, the file
state hook will be updated with the chosen file information.
Fill in the caption
//PostUpload.js
//...
const [caption, setCaption] = useState("")
//...
<Input
className="child"
type="text"
name="upload-caption"
placeholder="write your caption here"
value={caption}
onChange={(e)=>setCaption(e.target.value)}
/>
//...
The user can write their caption through the input box in <Input/>
element. In any letter the user input, the caption
state hook will be get updated as well.
Upload to Firebase
// PostUpload.jsx
import { db, storage } from '../firebase/firebase';
//...
const [progress, setProgress] = useState(0)
// this state is updated by chooseFile function
const [file, setFile] = useState(null)
//...
const uploadFile = () => {
if (file === null){
alert('Please choose a file!')
return;
};
const imageName = file.name;
//Store the post Information into Firestore
const addPost = function(caption, username, url) {
const newPost = {
timestamp: firebase
.firestore
.FieldValue
.serverTimestamp(),
caption: caption,
username: username,
imageURL: url
}
db.collection('posts').add(newPost)
};
// Save the uploaded picture into Firebase storage
const uploadTask = storage
.ref(`images/${imageName}`)
.put(file)
//...
<Button className="child" onClick={uploadFile}>Upload</Button>
The post upload separated into two main things:
- store the post information (caption, username, etc) into Firestore. Handled by
addPost
. - save the uploaded picture into Firebase storage. This task is done by
uploadTask
.
Store the post Information into Firestore
newPost
defines the post information that we want to store in Firestore. There are 4 things that we want to know for each post:
-
timestamp
: the value is obtained from the firebase library. This represents the upload time -
caption
: obtained from user input for this post's caption -
username
: I put the value asuploader
in this example. However, in our main project, this contains the information of logged in user. -
imageURL
: This post's uploaded picture URL in Firebase storage. We will get the value after the picture has been successfully uploaded.
db.collection('posts').add(newPost)
We can simply call the code above to add our data into Firestore.
db.collection('posts')
specifies which collection that we are referring to. In this example, I store the post's information into the "posts" collection.
Then, we can add our new post into this collection by chaining the collection with add
method plus newPost
that we just defined previously as the argument.
Notice that we only declare this function and haven't called it yet. We want the post information to be stored only if the picture uploading process has been finished.
I will mention this addPost
function again later in the progress bar section.
Save the uploaded picture into Firebase storage
We cannot only use Firestore in this example. Firestore only supports text-based information. The uploaded picture is needed to be stored somewhere else. We will use Firebase storage in this example.
storage.ref(`images/${imageName}`).put(file)
The uploaded picture information is already stored in our file
state hook. We can use storage.ref
to tell which target directory and filename in the storage. In this example, I chose images/{filename}
as the file reference. We can then chain this with put
method and use file
as the argument.
Progress Bar
The upload process may need to take some time to be finished, depending on the picture size and internet speed. For a better user experience, we can give a visual hint on how the upload process is going. One of the best ways is through the progress bar.
Firebase storage supports these needs by checking on how many bytes that the picture has been transferred.
//PostUpload.jsx
//...
const [progress, setProgress] = useState(0)
//...
uploadTask.on(
"state_changed",
(snapshot) => {
const progressNum = Math.round(
(snapshot.bytesTransferred/ snapshot.totalBytes)* 100
);
setProgress(progressNum);
},
(error) => {
console.log(error);
alert(error.message);
},
() => {
storage
.ref('images')
.child(imageName)
.getDownloadURL()
.then(url => {
addPost(caption, username, URL)
})
Notice that we reuse the upload task that we previously stored in uploadTask
. In every state_changed
of the uploading task, the progress
state hook will be get updated. The value of this progress
is calculated by: bytesTransferred
of the snapshot divided by the totalBytes
of the uploaded picture.
After the image has been completely uploaded, the second callback function is triggered. Remember addPost
function we have defined previously? Here, the post information is stored in Firebase along with its uploaded picture URL.
Post Download
Besides post uploading, users can also see all of the uploaded posts in the system. Previously, I experimented with the real-time connection in Firebase, however, I can't find a way to paginate and limit the query. I ended up using a simple query and limit the post on every page.
//App.jsx
import { db } from "./firebase/firebase";
//...
const [posts, setPosts] = useState([])
//...
const fetchData = () => {
db
.collection('posts')
.orderBy('timestamp','desc')
.limit(10)
.get().then(snapshot=>{
if (snapshot.docs.length === 0);
setPosts([...posts, ...snapshot.docs.map(doc=> (
{id: doc.id,
post: doc.data()}
))])
})
}
useEffect(() => {
fetchData();
}, [])
All of the posts is stored in posts
state hook. We retrieve the documents by get
method from "posts" collection, order them by "timestamp" attribute descendingly and limit the query by 10 posts. get
method returns an Async Promise so we need to chain it with updating the posts
function.
In this example, we only call fetchData
once, when the user opens the app. In the latter example, We can update it in every user scroll.
Image Compression
Yay! We've implemented all of the needed features for uploading posts. But hey, Upload the picture and download the picture need to take some time to be finished. What's wrong? Turns out the picture we upload and download follows the original size from the uploader. We can see this as another room for improvement.
You can see the difference in picture loading before and after compression:
The picture on the left takes a longer time to load than the picture on the right. The picture compression reduces the load time by almost half the time.
Here's how I compress the picture (disclaimer: it's a modified script from stackoverflow):
// resizer.js
async function resizeMe(img) {
var max_width = 500;
var max_height = 500;
var canvas = document.createElement('canvas');
const bitmap = await createImageBitmap(img)
var width = bitmap.width;
var height = bitmap.height;
// calculate the width and height, constraining the proportions
if (width > height) {
if (width > max_width) {
height = Math.round(height *= max_width / width);
width = max_width;
}
} else {
if (height > max_height) {
width = Math.round(width *= max_height / height);
height = max_height;
}
}
// resize the canvas and draw the image data into it
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0, width, height);
var blobBin = atob(canvas.toDataURL("image/jpeg", 0.7).split(',')[1]);
var array = [];
for(var i = 0; i < blobBin.length; i++) {
array.push(blobBin.charCodeAt(i));
}
var file = new Blob([new Uint8Array(array)], {type: 'image/png'});
return file; // get the data from canvas as 70% JPG (can be also PNG, etc.)
}
export default resizeMe;
This script creates a hidden canvas
element to resize the picture. Here we set the max-width and the max height to 500. The script keeps the aspect ratio of the image and gets 70% of the quality of the resized picture. Because we need to wait for the image to be resized before passing the image to the uploading process, we need to set this function as an async function.
We then can simply call this function in our PostUpload.jsx
. Don't forget, to put async
in the uploadFile
to await call resizer
function.
// PostUpload.jsx
import resizer from "../utils/resizer";
// ...
const uploadFile = async () => {
// ...
const uploadTask = storage.ref(`images/${imageName}`).put(await resizer(file))
//...
}
Conclusion
Woo hoo! If you've reached this point, we have created a powerful post sharing web app!
Implementing the app is a breeze with the help of Firebase Firestore and Storage. In this example, we can see how Firebase Firestore helps us to store posts' information in JSON format, and store the image link from Firebase storage.
We also optimized the web app by compressing the picture size before it's get uploaded to shorten the picture upload and download process.
Do you have any better way to upload/ download and resize the picture? Leave your solution below!
Top comments (0)