DEV Community

loading...
Cover image for Full Stack Instagram: Post Upload

Full Stack Instagram: Post Upload

Arnold Samuel Chan
Data Engineer, Full Stack Engineer, Python Developer, Data Scientist Profound at making the most of your data. Love to work remotely Speaks in Python and Javascript.
・7 min read

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

Apr-28-2021_23-04-50

Overview

User flow

  1. As a user, they can upload a post by:
    • Choose a picture
    • Fill in the caption
    • Hit the upload button
  2. The progress bar shows the uploading process.
  3. The new uploaded post will show in the timeline
  4. User can see all of the uploaded posts

File tree:

Untitled

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 }
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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)}
/>
//...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 as uploader 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
       })
Enter fullscreen mode Exit fullscreen mode

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();
  }, [])
Enter fullscreen mode Exit fullscreen mode

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:

ezgif.com-gif-maker

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;
Enter fullscreen mode Exit fullscreen mode

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))
//...
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)