Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Build a Google Classroom clone with React and Firebase

20 min read 5703

Now that the entire world is facing a pandemic, educational institutions like schools and colleges are switching towards a completely online teaching experience. There are many services on the internet that assist teachers with this, like Google Classroom, Microsoft Teams, and Zoom.

Now, it seems that new services are cropping up every day that claim to improve the teaching process. That’s why I thought it would be useful to know how to make your own Google Classroom clone!

This tutorial will teach you how to work with React and Firebase so that you can put all of the pieces together to make a banger application.

What will you require?

  • A code editor. I recommend Visual Studio Code, because it has an integrated terminal
  • NodeJS installed, because we are using React
  • A Google account to use Firebase
  • Working knowledge of React – I would not recommend this tutorial for beginners

Creating a React app

Now, let’s start working on this fun build! To create a React app, open a terminal in a safe folder and enter the following command; npm will do rest of the work for you:

npx create-react-app app-name

Remember to replace app-name with the actual name you want to give the build. In my case, I’ve named it google-classroom-clone.

Once the app is installed, open Visual Studio Code in that directory. Then, open the Integrated Terminal (Ctrl+J in Windows, and Cmd+J in MacOS) and type the following command to fire up the app:

npm start

If you see the following screen, you have successfully created your React app:

Screenshot of blank React app

Now, let’s do a quick clean up of the files. You can delete the following from the project; we don’t need them:

  • logo.svg
  • setupTests.svg
  • App.test.js

Go ahead and open App.js to remove the logo.svg import at the top, along with everything under the div tag in the file. Your file should now look like this:

We made a custom demo for .
No really. Click here to check it out.

import "./App.css";
function App() {
  return <div className="app"></div>;
}
export default App;

Remove the contents of App.css because we don’t need the default styling React gives us.

Next, type the following in the terminal to install the dependencies that will help us throughout the project:

npm install firebase moment react-firebase-hooks recoil @material-ui/core @material-ui/icons react-router-dom

firebase helps us interact with Firebase services with ease, and moment helps us with dates in our project. Because we are using function-based React components, react-firebase-hooks provides various hooks that inform us about the user’s state.

recoil is a state management library as uncomplicated as Redux, so I decided we could use this and keep things simple.

@material-ui/core and@material-ui/icons provide us with various pre-built components and SVG icons. Finally, react-router-dom helps us with routes.

Setting up Firebase

We will be using Firebase as the backend, so have your Google account details ready. Now, go to the Firebase console and log in. Now, click on Add Project and you should see the following screen:

Screenshot of Firebase create project screen

Enter a project name. In my case, I’ll use google-classroom-clone-article.

Now you will be prompted if you want to enable Google Analytics for your project. Although we don’t really need Google Analytics, there’s no harm in keeping it enabled. Remember to choose Default account for Firebase when asked to choose an account. Click on Create Project.

Now Firebase will allocate resources for the project. Once it’s done, press Continue to proceed to your project dashboard. Now, let’s set up a few things in the Firebase dashboard.

Enabling authentication

In the sidebar, click on Authentication and you’ll be presented with the following screen:

Screenshot of Firebase Authentication screen

Click on Get started. This will enable the authentication module for you, and you should see the various authentication options available:

Screenshot of Firebase authentication sign-in providers list

In this we are going to use Google authentication, so click on Google, press Enable, fill in the required details, and click Save.

You have successfully set up Google authentication in your Firebase project.

Enabling Cloud Firestore

Firebase’s Cloud Firestore is a non-relational database just like MongoDB. To enable Cloud Firestore, click Firestore Database in the sidebar:

Screenshot of Cloud Firestore homepage

Click on Create Database and you will be prompted with the following modal:

Screenshot of create database page in Cloud Firestore

Remember to start the Firestore Database in test mode. This is because we don’t want to worry about a production environment and security rules, in order to focus more on the development side of things. However, you can change it to production mode after you have completed the project.

Click on Next, choose a database location, and press Create. This will finally initialize your Cloud Firestore Database.

Now let’s copy our Firebase config. Click the gear icon on the sidebar and go to project settings. Scroll down and you will see this section:

Screenshot of Firebase project settings that says "there are no apps in your project"

Click on the third icon (</>), which represents a web application. Provide a name for the app and click on Register app. Ignore every other step, we will do it manually.

Now, return to project settings, and copy the config. It should look like this:

Screenshot of SDK setup and configuration screen

Linking a React app with Firebase

Now that everything’s set up, we finally get to the fun part of coding! In your React app, create a new file named firebase.js and import the firebase package using this statement:

import firebase from "firebase";

Now paste in the config. We need to initialize the app so that our React app can communicate with Firebase. To do so, use the following code:

const app = firebase.initializeApp(firebaseConfig);
const auth = app.auth();
const db = app.firestore();

In the above code, we are initiating a connection with Firebase using the initializeApp() function. Then, we are extracting the auth and firestore modules from our app and storing them in separate variables.

Now let’s set up a few functions to help us in the app:

const googleProvider = new firebase.auth.GoogleAuthProvider();

// Sign in and check or create account in firestore
const signInWithGoogle = async () => {
  try {
    const response = await auth.signInWithPopup(googleProvider);
    console.log(response.user);
    const user = response.user;
    console.log(`User ID - ${user.uid}`);
    const querySnapshot = await db
      .collection("users")
      .where("uid", "==", user.uid)
      .get();
    if (querySnapshot.docs.length === 0) {
      // create a new user
      await db.collection("users").add({
        uid: user.uid,
        enrolledClassrooms: [],
      });
    }
  } catch (err) {
    alert(err.message);
  }
};

const logout = () => {
  auth.signOut();
};

In the above code, we are getting the GoogleAuthProvider provided by Firebase to help us with Google authentication. If there is any error in the authentication, the user will automatically be transferred to the catch block where the error will be displayed on the screen.

We are using the signInWithPopup() function by Firebase and passing in the Google provider in order to tell Firebase that we want to log in through an external provider. In this case, it’s Google.

Next, we are checking our Firestore database to see if the authenticated user exists in our database or not. If not, we create a new entry in our database so that we consider the user registered.

Firebase is very clever at handling local storage. The authentication will persist on page reload, and Firebase handles it under the hood. So we do not need to do anything more once the user is authenticated.

Now, we create a logout() function.

Finally, export these modules so that we can use them throughout the app:

export { app, auth, db, signInWithGoogle, logout };

Now let’s proceed to configuring router.

Configuring React Router

We need to configure our React app so that it can handle multiple routes with multiple screens. react-router-dom helps us with this.

Go to App.js and import the necessary components from this package:

import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

Now, inside the empty <div>, add the following configuration:

<Router>
  <Switch>
    <Route exact path="/">
      Hello
    </Route>
  </Switch>
</Router>

You probably have worked with React Router before because it’s used in almost all multi-page projects. But if you don’t know about it, no worries, let’s break down the above code and see what’s happening here.

All of your code must be enclosed under <Router>. This helps the package keep track of pages and components.

<Switch> tells the router that this part needs to be changed on a page change. So, some components should only be shown if the user is on a certain page. In our case, we are switching screens, because that’s what users normally do.

Components enclosed under <Route> get rendered when the user is on the route specified in path.

Now, if you notice, you can see Hello on your screen. But if you change URL to something like http://localhost:3000/test, you will see Hello no longer appears. That’s the power of React Router.

Creating a new component

Let’s make a new component. I highly recommend installing the ES7 React Snippets extension into VS Code. It will help you make React components very easily.

Create a new folder named screens and make two files called Home.js and Home.css. Go to Home.js, start typing rfce, and press Enter. A brand new React component will be made. Import the CSS file by including this statement at the top:

import "./Home.css";

We will always use this method to create components. Let’s go back to App.js and add the home page to our home route. Do not forget to import the component like this or you will face errors:

import Home from "./components/Home";

Your JSX in App.js should look like this:

<Router>
  <Switch>
    <Route exact path="/">
      <Home />
    </Route>
  </Switch>
</Router>

Now go to Home.js and add the following layout:

<div className="home">
  <div className="home__container">
    <img
      src="https://upload.wikimedia.org/wikipedia/commons/5/59/Google_Classroom_Logo.png"
      alt="Google Classroom Image"
      className="home__image"
    />
    <button className="home__login">
      Login with Google
    </button>
  </div>
</div>

We will not be focusing on styling in this tutorial, so use the following CSS for the Home.css file:

.home {
  height: 100vh;
  width: 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
}
.home__container {
  background-color: #f5f5f5;
  box-shadow: 0px 0px 2px -1px black;
  display: flex;
  flex-direction: column;
  padding: 30px;
}
.home__login {
  margin-top: 30px;
  padding: 20px;
  background-color: #2980b9;
  font-size: 18px;
  color: white;
  border: none;
  text-transform: uppercase;
  border-radius: 10px;
}

Once saved, you should see a screen like this:

Screenshot of example app with Google Classroom logo and text that says "login using google"

Implementing Google login functionality

We already made the function in firebase.js to handle authentication; now we can implement it.

Add the signInWithGoogle function to the “Log in with Google” button’s onClick method. Don’t forget to import the signInWithGoogle function. Now your JSX should look like this:

<div className="home">
  <div className="home__container">
    <img
      src="https://upload.wikimedia.org/wikipedia/commons/5/59/Google_Classroom_Logo.png"
      alt="Google Classroom Image"
      className="home__image"
    />
    <button className="home__login" onClick={signInWithGoogle}>
      Login with Google
    </button>
  </div>
</div>

When a user signs in, we want to redirect them to the dashboard with the React Router and Firebase hooks. The Firebase hooks always monitor if the authentication state of the user has changed, so we can use that data to check if the user has logged in. Let’s import Firebase and router hooks like so:

import { useAuthState } from "react-firebase-hooks/auth";
import { useHistory } from "react-router-dom";

Then, in your component, add the following lines in order to use the hooks:

const [user, loading, error] = useAuthState(auth);
const history = useHistory();

The first line gives us the state of the user. So, if the user is in loading state, loading is true. If the user is not logged in, user will be undefined, or there will be no user data. If there’s any error, it will be stored in error.

The history now gives us access to routing the user through our code without them clicking a link. We can use methods like history.push() and history.replace() to manage routing.

Finally, let’s make a useEffect() hook that will redirect the user once they are authenticated:

useEffect(() => {
  if (loading) return;
  if (user) history.push("/dashboard");
}, [loading, user]);

The above code checks if the user is logged in whenever the user state changes. If so, he is redirected to the /dashboard route. The reason I used useEffect() is because I can check the state of the user whenever the authentication states update. So, if a user who is logged in visits the homepage, he will be redirected to the dashboard immediately without showing the login screen.

Now if you try to log in with your Google account, you’ll see a blank screen because we don’t have a dashboard yet. But before creating a dashboard, we will create a navbar, which will be common in most of our screens.

Creating a navbar

Create a new folder in the src directory named components, then create a new component named Navbar along with JS and CSS files. Place the Navbar in our /dashboard route in App.js like this:

<Route exact path="/dashboard">
  <Navbar />
</Route>

Now, if you’re logged in, the navbar component is placed. Let’s add the basic layout. First, add the Firebase hooks, as we will need them to fetch user data:

const [user, loading, error] = useAuthState(auth);

Your file should look like this:

import { Avatar, IconButton, MenuItem, Menu } from "@material-ui/core";
import { Add, Apps, Menu as MenuIcon } from "@material-ui/icons";
import React, { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, logout } from "../firebase";
import "./Navbar.css";
function Navbar() {
  const [user, loading, error] = useAuthState(auth);
  const [anchorEl, setAnchorEl] = useState(null);

  const handleClick = (event) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <>
      <CreateClass />
      <JoinClass />
      <nav className="navbar">
        <div className="navbar__left">
          <IconButton>
            <MenuIcon />
          </IconButton>
          <img
            src="https://1000logos.net/wp-content/uploads/2021/05/Google-logo.png"
            alt="Google Logo"
            className="navbar__logo"
          />{" "}
          <span>Classroom</span>
        </div>
        <div className="navbar__right">
          <IconButton
            aria-controls="simple-menu"
            aria-haspopup="true"
            onClick={handleClick}
          >
            <Add />
          </IconButton>
          <IconButton>
            <Apps />
          </IconButton>
          <IconButton onClick={logout}>
            <Avatar src={user?.photoURL} />
          </IconButton>
          <Menu
            id="simple-menu"
            anchorEl={anchorEl}
            keepMounted
            open={Boolean(anchorEl)}
            onClose={handleClose}
          >
            <MenuItem>
              Create Class
            </MenuItem>
            <MenuItem>
              Join Class
            </MenuItem>
          </Menu>
        </div>
      </nav>
    </>
  );
}
export default Navbar;

Add the following styling to Navbar.css:

.navbar {
  width: 100vw;
  height: 65px;
  border-bottom: 1px solid #dcdcdc;
  display: flex;
  justify-content: space-between;
  padding: 0 20px;
  align-items: center;
}
.navbar__left {
  display: flex;
  align-items: center;
}
.navbar__left img {
  margin-right: 20px;
  margin-left: 20px;
}
.navbar__left span {
  font-size: 20px;
}
.navbar__right {
  display: flex;
  align-items: center;
}
.navbar__logo {
  height: 30px;
  width: auto;
}

We used Material UI components along with my own styling, so your navbar should look like this:

Screenshot of basic Material UI navbar in Google Classroom

If you click the + icon, you should see a menu pop up:

Screenshot of a menu that has "create class" and "join class" options

For now, clicking any of the options will not do anything. Let’s make new components that will act as modals for creating and joining a class. For that, we need state management to determine if the modal is open or closed.

We have many components, so the recoil state management library is used to elevate data for every component to access. Create a new folder in src named utils and create a new file called atoms.js. This file should look like the following:

import { atom } from "recoil";
const joinDialogAtom = atom({
  key: "joinDialogAtom",
  default: false,
});
const createDialogAtom = atom({
  key: "createDialogAtom",
  default: false,
});
export { createDialogAtom, joinDialogAtom };

Atoms are just spaces to store your global data. Here, we have made two global atoms which indicate if the “join” or “create” modals are open. By default, they are always false.

Now let’s work on creating a class.

Creating a class

Create a new component in the components folder called CreateClass. We don’t need a CSS file for this one because we are using Material UI components:

import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
} from "@material-ui/core";
import React, { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useRecoilState } from "recoil";
import { auth, db } from "../firebase";
import { createDialogAtom } from "../utils/atoms";
function CreateClass() {
  const [user, loading, error] = useAuthState(auth);
  const [open, setOpen] = useRecoilState(createDialogAtom);
  const [className, setClassName] = useState("");
  const handleClose = () => {
    setOpen(false);
  };

  return (
    <div>
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        <DialogTitle id="form-dialog-title">Create class</DialogTitle>
        <DialogContent>
          <DialogContentText>
            Enter the name of class and we will create a classroom for you!
          </DialogContentText>
          <TextField
            autoFocus
            margin="dense"
            label="Class Name"
            type="text"
            fullWidth
            value={className}
            onChange={(e) => setClassName(e.target.value)}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Cancel
          </Button>
          <Button onClick={handleClose} color="primary">
            Create
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}
export default CreateClass;

Here, we are importing our Recoil atoms and retrieving the data inside of them, which, in our case, is a boolean that tells if our modal is open or not.

We are also syncing a state with the text box so that we can capture data from it at any time.

The open prop on <Dialog> comes from the state, so if the opened state is set to true, the modal will appear.

We have two buttons that will close the modal by setting our opened state to false.

Now, let’s create a function that will create a class by contacting our Cloud Firestore database:

const createClass = async () => {
  try {
    const newClass = await db.collection("classes").add({
      creatorUid: user.uid,
      name: className,
      creatorName: user.displayName,
      creatorPhoto: user.photoURL,
      posts: [],
    });
    const userRef = await db
      .collection("users")
      .where("uid", "==", user.uid)
      .get();
    const docId = userRef.docs[0].id;
    const userData = userRef.docs[0].data();
    let userClasses = userData.enrolledClassrooms;
    userClasses.push({
      id: newClass.id,
      name: className,
      creatorName: user.displayName,
      creatorPhoto: user.photoURL,
    });
    const docRef = await db.collection("users").doc(docId);
    await docRef.update({
      enrolledClassrooms: userClasses,
    });
    handleClose();
    alert("Classroom created successfully!");
  } catch (err) {
    alert(`Cannot create class - ${err.message}`);
  }
};

In this function, we are using a try-catch block so we can handle any errors captured while contacting Firebase.

We are creating a new entry in the collection classes with the data we get from our text box state and Firebase hooks, then getting the user’s data by fetching through our database using the user ID.

We’re also adding the class ID to our user’s enrolledClasses array and updating our user data in the database.

Now insert this function in the onClick of the Create button. Your JS file should look like this:

import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
} from "@material-ui/core";
import React, { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useRecoilState } from "recoil";
import { auth, db } from "../firebase";
import { createDialogAtom } from "../utils/atoms";
function CreateClass() {
  const [user, loading, error] = useAuthState(auth);
  const [open, setOpen] = useRecoilState(createDialogAtom);
  const [className, setClassName] = useState("");
  const handleClose = () => {
    setOpen(false);
  };
  const createClass = async () => {
    try {
      const newClass = await db.collection("classes").add({
        creatorUid: user.uid,
        name: className,
        creatorName: user.displayName,
        creatorPhoto: user.photoURL,
        posts: [],
      });
      // add to current user's class list
      const userRef = await db
        .collection("users")
        .where("uid", "==", user.uid)
        .get();
      const docId = userRef.docs[0].id;
      const userData = userRef.docs[0].data();
      let userClasses = userData.enrolledClassrooms;
      userClasses.push({
        id: newClass.id,
        name: className,
        creatorName: user.displayName,
        creatorPhoto: user.photoURL,
      });
      const docRef = await db.collection("users").doc(docId);
      await docRef.update({
        enrolledClassrooms: userClasses,
      });
      handleClose();
      alert("Classroom created successfully!");
    } catch (err) {
      alert(`Cannot create class - ${err.message}`);
    }
  };
  return (
    <div>
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        <DialogTitle id="form-dialog-title">Create class</DialogTitle>
        <DialogContent>
          <DialogContentText>
            Enter the name of class and we will create a classroom for you!
          </DialogContentText>
          <TextField
            autoFocus
            margin="dense"
            label="Class Name"
            type="text"
            fullWidth
            value={className}
            onChange={(e) => setClassName(e.target.value)}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Cancel
          </Button>
          <Button onClick={createClass} color="primary">
            Create
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}
export default CreateClass;

Joining a class

The basic concept of joining a class is very similar to creating a class. Here’s how JoinClass.js should look:

import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  TextField,
} from "@material-ui/core";
import React, { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useRecoilState } from "recoil";
import { auth, db } from "../firebase";
import { joinDialogAtom } from "../utils/atoms";
function JoinClass() {
  const [open, setOpen] = useRecoilState(joinDialogAtom);
  const [user, loading, error] = useAuthState(auth);
  const [classId, setClassId] = useState("");
  const handleClose = () => {
    setOpen(false);
  };
  const joinClass = async () => {
    try {
      // check if class exists
      const classRef = await db.collection("classes").doc(classId).get();
      if (!classRef.exists) {
        return alert(`Class doesn't exist, please provide correct ID`);
      }
      const classData = await classRef.data();
      // add class to user
      const userRef = await db.collection("users").where("uid", "==", user.uid);
      const userData = await (await userRef.get()).docs[0].data();
      let tempClassrooms = userData.enrolledClassrooms;
      tempClassrooms.push({
        creatorName: classData.creatorName,
        creatorPhoto: classData.creatorPhoto,
        id: classId,
        name: classData.name,
      });
      await (
        await userRef.get()
      ).docs[0].ref.update({
        enrolledClassrooms: tempClassrooms,
      });
      // alert done
      alert(`Enrolled in ${classData.name} successfully!`);
      handleClose();
    } catch (err) {
      console.error(err);
      alert(err.message);
    }
  };
  return (
    <div className="joinClass">
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        <DialogTitle id="form-dialog-title">Join class</DialogTitle>
        <DialogContent>
          <DialogContentText>
            Enter ID of the class to join the classroom
          </DialogContentText>
          <TextField
            autoFocus
            margin="dense"
            label="Class Name"
            type="text"
            fullWidth
            value={classId}
            onChange={(e) => setClassId(e.target.value)}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Cancel
          </Button>
          <Button onClick={joinClass} color="primary">
            Join
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}
export default JoinClass;

The difference here is that we are using the other atom, which checks if the “join class” modal is opened or not, and if the class exists. If so, we add it to the user’s enrolledClasses array and update the user in Firestore. It’s so simple!

Now, we need to link everything together in the navbar and set the onClick function. Here’s how your Navbar.js file should look:

import { Avatar, IconButton, MenuItem, Menu } from "@material-ui/core";
import { Add, Apps, Menu as MenuIcon } from "@material-ui/icons";
import React, { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useRecoilState } from "recoil";
import { auth, logout } from "../firebase";
import { createDialogAtom, joinDialogAtom } from "../utils/atoms";
import CreateClass from "./CreateClass";
import JoinClass from "./JoinClass";
import "./Navbar.css";
function Navbar() {
  const [user, loading, error] = useAuthState(auth);
  const [anchorEl, setAnchorEl] = useState(null);
  const [createOpened, setCreateOpened] = useRecoilState(createDialogAtom);
  const [joinOpened, setJoinOpened] = useRecoilState(joinDialogAtom);
  const handleClick = (event) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };
  return (
    <>
      <CreateClass />
      <JoinClass />
      <nav className="navbar">
        <div className="navbar__left">
          <IconButton>
            <MenuIcon />
          </IconButton>
          <img
            src="https://1000logos.net/wp-content/uploads/2021/05/Google-logo.png"
            alt="Google Logo"
            className="navbar__logo"
          />{" "}
          <span>Classroom</span>
        </div>
        <div className="navbar__right">
          <IconButton
            aria-controls="simple-menu"
            aria-haspopup="true"
            onClick={handleClick}
          >
            <Add />
          </IconButton>
          <IconButton>
            <Apps />
          </IconButton>
          <IconButton onClick={logout}>
            <Avatar src={user?.photoURL} />
          </IconButton>
          <Menu
            id="simple-menu"
            anchorEl={anchorEl}
            keepMounted
            open={Boolean(anchorEl)}
            onClose={handleClose}
          >
            <MenuItem
              onClick={() => {
                setCreateOpened(true);
                handleClose();
              }}
            >
              Create Class
            </MenuItem>
            <MenuItem
              onClick={() => {
                setJoinOpened(true);
                handleClose();
              }}
            >
              Join Class
            </MenuItem>
          </Menu>
        </div>
      </nav>
    </>
  );
}
export default Navbar;

Creating the dashboard

Now let’s get on the dashboard. Create a new component in the screens folder named Dashboard, remembering to make JS and CSS files for it. Here’s the styling for Dashboard.css:

.dashboard__404 {
  display: flex;
  height: 100vh;
  width: 100vw;
  align-items: center;
  justify-content: center;
  font-size: 20px;
}
.dashboard__classContainer {
  display: flex;
  padding: 30px;
  flex-wrap: wrap;
  width: 100vw;
}

Now, let’s first make a component that will display the individual classes. It will be nothing special, just rendering out data with styles. Create a new component called ClassCard in components and copy this layout:

import { IconButton } from "@material-ui/core";
import { AssignmentIndOutlined, FolderOpenOutlined } from "@material-ui/icons";
import React from "react";
import { useHistory } from "react-router-dom";
import "./ClassCard.css";
function ClassCard({ name, creatorName, creatorPhoto, id, style }) {
  const history = useHistory();
  const goToClass = () => {
    history.push(`/class/${id}`);
  };
  return (
    <div className="classCard" style={style} onClick={goToClass}>
      <div className="classCard__upper">
        <div className="classCard__className">{name}</div>
        <div className="classCard__creatorName">{creatorName}</div>
        <img src={creatorPhoto} className="classCard__creatorPhoto" />
      </div>
      <div className="classCard__middle"></div>
      <div className="classCard__lower">
        <IconButton>
          <FolderOpenOutlined />
        </IconButton>
        <IconButton>
          <AssignmentIndOutlined />
        </IconButton>
      </div>
    </div>
  );
}
export default ClassCard;

Here we are just taking in props and rendering it out. One thing to note is that when the user presses on the card component, she gets redirected to the class screen.

Here’s ClassCard.css:

.classCard__upper {
  background-color: #008d7d;
  height: 90px;
  position: relative;
  color: white;
  padding: 10px;
  border-bottom: 1px solid #dcdcdc;
}
.classCard {
  width: 300px;
  border: 1px solid #dcdcdc;
  border-radius: 5px;
  overflow: hidden;
  cursor: pointer;
}
.classCard__middle {
  height: 190px;
  border-bottom: 1px solid #dcdcdc;
}
.classCard__creatorPhoto {
  position: absolute;
  right: 5px;
  border-radius: 9999px;
}
.classCard__className {
  font-weight: 600;
  font-size: 30px;
}
.classCard__creatorName {
  position: absolute;
  bottom: 12px;
  font-size: 15px;
}
.classCard__lower {
  display: flex;
  flex-direction: row-reverse;
}

Now, let’s include the Dashboard component into our App.js file:

<Router>
  <Switch>
    <Route exact path="/">
      <Home />
    </Route>
    <Route exact path="/dashboard">
      <Navbar />
      <Dashboard />
    </Route>
  </Switch>
</Router>

Now open Dashboard.js and make a function to fetch all of the user’s classes:

const fetchClasses = async () => {
  try {
    await db
      .collection("users")
      .where("uid", "==", user.uid)
      .onSnapshot((snapshot) => {
        setClasses(snapshot?.docs[0]?.data()?.enrolledClassrooms);
      });
  } catch (error) {
    console.error(error.message);
  }
};

This code is similar to when we fetched data from Firestore. We set up a snapshot listener so that whenever data updates in the Firestore database, the changes are reflected here.

Now that we have the classes in a state, we can easily render them out. Here’s what your Dashboard.js file should look like:

import React, { useEffect } from "react";
import "./Dashboard.css";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, db } from "../firebase";
import { useHistory } from "react-router-dom";
import { useState } from "react";
import ClassCard from "../components/ClassCard";
function Dashboard() {
  const [user, loading, error] = useAuthState(auth);
  const [classes, setClasses] = useState([]);
  const history = useHistory();
  const fetchClasses = async () => {
    try {
      await db
        .collection("users")
        .where("uid", "==", user.uid)
        .onSnapshot((snapshot) => {
          setClasses(snapshot?.docs[0]?.data()?.enrolledClassrooms);
        });
    } catch (error) {
      console.error(error.message);
    }
  };
  useEffect(() => {
    if (loading) return;
    if (!user) history.replace("/");
  }, [user, loading]);
  useEffect(() => {
    if (loading) return;
    fetchClasses();
  }, [user, loading]);
  return (
    <div className="dashboard">
      {classes?.length === 0 ? (
        <div className="dashboard__404">
          No classes found! Join or create one!
        </div>
      ) : (
        <div className="dashboard__classContainer">
          {classes.map((individualClass) => (
            <ClassCard
              creatorName={individualClass.creatorName}
              creatorPhoto={individualClass.creatorPhoto}
              name={individualClass.name}
              id={individualClass.id}
              style={{ marginRight: 30, marginBottom: 30 }}
            />
          ))}
        </div>
      )}
    </div>
  );
}
export default Dashboard;

Now, if you create a few classes, you should see them populate on the page:

Screenshot of three test classes within Google Classroom

Congrats! Our dashboard is ready. Now we need to make a class screen where all the announcements from each class will be displayed.

Creating the class screen

First let’s create a component that will help us create the class screen. We need to display announcements from a class, so we make an Announcement component that will take in the props and render out the data.

Copy the following into your Announcement.js file:

import { IconButton } from "@material-ui/core";
import { Menu, MoreVert } from "@material-ui/icons";
import React from "react";
import "./Announcement.css";
function Announcement({ image, name, date, content, authorId }) {
  return (
    <div className="announcement">
      <div className="announcement__informationContainer">
        <div className="announcement__infoSection">
          <div className="announcement__imageContainer">
            <img src={image} alt="Profile photo" />
          </div>
          <div className="announcement__nameAndDate">
            <div className="announcement__name">{name}</div>
            <div className="announcement__date">{date}</div>
          </div>
        </div>
        <div className="announcement__infoSection">
          <IconButton>
            <MoreVert />
          </IconButton>
        </div>
      </div>
      <div className="announcement__content">{content}</div>
    </div>
  );
}
export default Announcement;

Nothing much happening here, just basic layouts. Here’s Announcement.css:

.announcement {
  width: 100%;
  padding: 25px;
  border-radius: 10px;
  border: 1px solid #adadad;
  margin-bottom: 20px;
}
.announcement__informationContainer {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.announcement__infoSection {
  display: flex;
  align-items: center;
}
.announcement__nameAndDate {
  margin-left: 10px;
}
.announcement__name {
  font-weight: 600;
}
.announcement__date {
  color: #424242;
  font-size: 14px;
  margin-top: 2px;
}
.announcement__imageContainer > img {
  height: 50px;
  width: 50px;
  border-radius: 9999px;
}
.announcement__content {
  margin-top: 15px;
}

Now, let’s create the class screen. Create a new component named Class in the screens folder. Let’s include it in our App.js:

<Router>
  <Switch>
    <Route exact path="/">
      <Home />
    </Route>
    <Route exact path="/dashboard">
      <Navbar />
      <Dashboard />
    </Route>
    <Route exact path="/class/:id">
      <Navbar />
      <Dashboard />
    </Route>
  </Switch>
</Router>

One thing to note here is :id, a query parameter we are sending through the URL. We can access this id in our class screen, thanks to React Router. Here’s the content for Class.css:

.class {
  width: 55%;
  margin: auto;
}
.class__nameBox {
  width: 100%;
  background-color: #0a9689;
  color: white;
  height: 350px;
  margin-top: 30px;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 30px;
  font-weight: bold;
  font-size: 43px;
}
.class__announce {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 20px;
  margin-bottom: 25px;
  box-shadow: 0px 1px 6px -2px black;
  justify-content: space-between;
  border-radius: 15px;
  margin-top: 20px;
}
.class__announce > img {
  height: 50px;
  width: 50px;
  border-radius: 9999px;
}
.class__announce > input {
  border: none;
  padding: 15px 20px;
  width: 100%;
  margin-left: 20px;
  margin-right: 20px;
  font-size: 17px;
  outline: none;
}

Now let’s focus on Class.js. Again, it’s the same as what we did with components before:

import { IconButton } from "@material-ui/core";
import { SendOutlined } from "@material-ui/icons";
import moment from "moment";
import React from "react";
import { useEffect } from "react";
import { useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import { useHistory, useParams } from "react-router-dom";
import Announcement from "../components/Announcement";
import { auth, db } from "../firebase";
import "./Class.css";
function Class() {
  const [classData, setClassData] = useState({});
  const [announcementContent, setAnnouncementContent] = useState("");
  const [posts, setPosts] = useState([]);
  const [user, loading, error] = useAuthState(auth);
  const { id } = useParams();
  const history = useHistory();
  useEffect(() => {
    // reverse the array
    let reversedArray = classData?.posts?.reverse();
    setPosts(reversedArray);
  }, [classData]);
  const createPost = async () => {
    try {
      const myClassRef = await db.collection("classes").doc(id).get();
      const myClassData = await myClassRef.data();
      console.log(myClassData);
      let tempPosts = myClassData.posts;
      tempPosts.push({
        authorId: user.uid,
        content: announcementContent,
        date: moment().format("MMM Do YY"),
        image: user.photoURL,
        name: user.displayName,
      });
      myClassRef.ref.update({
        posts: tempPosts,
      });
    } catch (error) {
      console.error(error);
      alert(`There was an error posting the announcement, please try again!`);
    }
  };
  useEffect(() => {
    db.collection("classes")
      .doc(id)
      .onSnapshot((snapshot) => {
        const data = snapshot.data();
        if (!data) history.replace("/");
        console.log(data);
        setClassData(data);
      });
  }, []);
  useEffect(() => {
    if (loading) return;
    if (!user) history.replace("/");
  }, [loading, user]);
  return (
    <div className="class">
      <div className="class__nameBox">
        <div className="class__name">{classData?.name}</div>
      </div>
      <div className="class__announce">
        <img src={user?.photoURL} alt="My image" />
        <input
          type="text"
          value={announcementContent}
          onChange={(e) => setAnnouncementContent(e.target.value)}
          placeholder="Announce something to your class"
        />
        <IconButton onClick={createPost}>
          <SendOutlined />
        </IconButton>
      </div>
      {posts?.map((post) => (
        <Announcement
          authorId={post.authorId}
          content={post.content}
          date={post.date}
          image={post.image}
          name={post.name}
        />
      ))}
    </div>
  );
}
export default Class;

There are a lot of things going on here, let’s break it down.

We are setting up a snapshot listener in one of the useEffect() hooks so that we get existing posts from the database. Then, we reverse the array and save it in another state. Reversing the posts will give us newest posts on top.

We are rendering the Announcement component according to posts. Once a person creates an announcement, the posts array is fetched from the database, a new entry is added, and the data is updated on database.

Because we have a snapshot listener, whenever we create a post, it will automatically get updated on the screen.

The class ID is in the URL bar. Other users can use the ID to join the class.

If everything is set up correctly, you should see something like this after adding a few posts:

Screenshot of test announcements in Google Classroom

Congrats! What’s next?

You’ve successfully made a Google Classroom clone using React and Firebase! Now you can play around with the code – try new things such as editing posts, or adding comments and attachments.

I’d also recommend making different clones. Exercises like this will help you understand how these popular apps work on a deeper level.

If you need the code of this clone, check out my GitHub repository where I’ve pushed all the code. You can make pull requests if you want to add more features.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

One Reply to “Build a Google Classroom clone with React and Firebase”

Leave a Reply