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.
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:
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:
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.
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:
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.
In the sidebar, click on Authentication and you’ll be presented with the following screen:
Click on Get started. This will enable the authentication module for you, and you should see the various authentication options available:
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.
Firebase’s Cloud Firestore is a non-relational database just like MongoDB. To enable Cloud Firestore, click Firestore Database in the sidebar:
Click on Create Database and you will be prompted with the following modal:
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:
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:
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.
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.
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:
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.
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:
If you click the + icon, you should see a menu pop up:
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.
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;
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;
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:
Congrats! Our dashboard is ready. Now we need to make a class screen where all the announcements from each class will be displayed.
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:
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
One Reply to "Build a Google Classroom clone with React and Firebase"
Moment.js is deprecated, you shouldn’t use it anymore.