Hola! In the first part, I've explained how I've made the data handling and authentication stuff. In this part I'll take you through building pages and components with those states and data. If you haven't had a chance to read part 1, click here
Creating context
What is a Context?
A context in the React app allows us to pass the data to the component tree without having to pass the data manually at every level.
Consider this case, I want to check the authentication status of the user in every page. The direct method to do this is to check the status on every page and render/redirect accordingly. Think of the time it would cost.
Instead of checking on every single page, you can do it one time and then store those information as a context. The data in the context is passed to all the components which share the context as the parent.
Creating and using the context in the app
React provides hooks such as createContext
and useContext
to create and use the context respectively
// src/contexts/app.js
import { createContext } from "react";
export const AppContext = createContext({});
Now to use the context, you'll have to include the context as the parent of the components where you'll need to check the status. Since in this case, I want to check the status on every page, I added the context as the root element.
// src/App.jsx
import AppContext from "contexts/app.js";
import { useState } from "react";
function App() {
const [session, setSession] = useState({ data: {}, loading: true });
return(
<div className="App">
<AppContext.Provider value={{ session, setSession }}>
{/* code here */}
</AppContext.Provider>
</div>
);
I created session
and setSession
with useState
hook. Now I can use the session
data and setSession
function from any component
Building Components
Layout
The basic layout of any website would have
- Header (with or without navigation bar)
- Content
- Footer
The header with the navigation bar is the trickier design in this website. Pulling it off required lot of CSS.
/* src/components/nav/Nav.css */
.navbar-brand-img {
border: 4px solid #d7a74f;
position: absolute;
border-radius: 50%;
height: auto;
width: 6rem;
}
.navbar-brand {
font-family: Rationale;
color: #000;
width: fit-content;
}
.navbar-rounded {
border-bottom-left-radius: 3rem;
border-bottom-right-radius: 3rem;
}
.active {
text-underline-offset: 3px;
text-decoration: underline !important;
text-decoration-color: #79201b !important;
}
@media screen and (max-width: 1000px) {
.navbar-nav {
width: 100%;
padding: 8rem 0 !important;
text-align: center;
}
.bg-secondary {
margin-top: 1rem;
}
.nav-item {
margin: 1vw
}
.navbar-brand-img {
position: sticky;
width: 4rem;
}
.navbar-brand {
width: 90%;
text-align: center;
}
}
.navbar-toggler {
border: 0;
border-radius: 0;
box-shadow: none !important;
}
.navbar-toggler[aria-expanded="true"] .navbar-toggler-icon {
background-image: url("~assets/images/icon_close.png");
opacity: 0.6
}
.navbar-toggler:focus {
box-shadow: 0 0 0 0
}
.navbar-dark .navbar-toggler {
border-color: rgba(0,0,0,0)
}
import { NavLink } from "react-router-dom";
import logo from "assets/images/logo.png";
import "./Nav.css";
const LINKS = [
{
to: "/candidates",
label: "Candidates",
},
{
to: "/vote",
label: "Vote"
},
{
to: "/profile",
label: "Info",
}
];
const NavigationDefault = () => {
return (
<div className="bg-color-maroon px-lg-5 pb-lg-4">
<nav className="navbar navbar-dark navbar-expand-lg py-0 mx-lg-5">
<div className="container-fluid bg-color-gold mx-lg-5 navbar-rounded">
<img src={logo} className="navbar-brand-img mt-lg-5 m-2 mx-lg-5" alt="" />
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul className="navbar-nav navbar-dark py-2 pb-5 pb-lg-2 px-5">
<li className="nav-item mx-3">
<NavLink
to={"/"}
exact
activeClassName="active"
className="nav-link text-uppercase text-white fw-bold"
id={"Home"}
>
Home
</NavLink>
</li>
{LINKS.map(({ to, label }) => {
return (
<li className="nav-item mx-3" key={to}>
<NavLink
to={to}
activeClassName="active"
className="nav-link text-uppercase text-white fw-bold"
id={label}
>
{label}
</NavLink>
</li>
)
})}
</ul>
</div>
</div>
</nav>
<div className="mx-lg-5 px-lg-5">
<div className="mx-lg-5 px-lg-5 d-flex justify-content-center d-lg-block">
<div className="navbar-brand bg-color-primary mx-5 mx-lg-3 p-0 px-5 navbar-rounded">
IIT Madras BS Students
</div>
</div>
</div>
</div>
);
};
export default NavigationDefault;
Then the Footer
// src/components/footer/Footer.jsx
import "./boxicons.css";
const Footer = () => {
return (
<footer className="bg-color-gold px-lg-5 py-4 font-roboto">
<div className="mx-lg-5 px-lg-5">
<a href="https://bit.ly/iitmelections2022" className="d-flex justify-content-center text-white" target={"_blank"} rel="noreferrer">https://bit.ly/iitmelections2022</a>
<a href="https://forms.gle/Tinh6czKqeu6YSBu7" className="d-flex justify-content-center text-white" target={"_blank"} rel="noreferrer">Support Form Link</a>
<div className="d-flex justify-content-center">
<a href="https://twitter.com/iitm_bsc" target={"_blank"} className="social fs-5 bx-border-circle d-flex align-items-center text-decoration-none d-flex align-items-center text-decoration-none" rel="noreferrer">
<i className="bx m-1 bxl-twitter"></i>
</a>
<a href="https://www.facebook.com/iitmadrasbscdegree/" target={"_blank"} className="social bx-border-circle d-flex align-items-center text-decoration-none fs-5" rel="noreferrer">
<i className="bx m-1 bxl-facebook"></i>
</a>
<a href="https://instagram.com/iitmadras_bsc?utm_medium=copy_link" target={"_blank"} className="fs-5 social bx-border-circle d-flex align-items-center text-decoration-none" rel="noreferrer">
<i className="bx m-1 bxl-instagram"></i>
</a>
<a href="https://www.linkedin.com/company/iit-madras-online-degree-programme" target={"_blank"} className="fs-5 social bx-border-circle d-flex align-items-center text-decoration-none" rel="noreferrer">
<i className="bx m-1 bxl-linkedin"></i>
</a>
</div>
<div className="font-righteous px-2 mt-3 text-white w-100 row">
<span className="text-start col-6">All rights reserved<br /><a href="mailto:webops@student.onlinedegree.iitm.ac.in" className="text-white" target={"_blank"} rel="noreferrer">WebOps 2022</a></span>
</div>
</div>
</footer>
);
};
export default Footer;
If you wonder what is in boxicons.css
, ask me in the comments 😉
Putting it all together in the Layout
// src/components/Layout.jsx
import NavigationDefault from "components/nav/Nav";
import Footer from "components/footer/Footer";
const Layout = ({ children }) => {
return (
<main className="bg-color">
<NavigationDefault />
<div style={{minHeight:"70vh"}}>{children}</div>
<Footer />
</main>
);
};
export default Layout;
Loader
No one likes to see a blank screen while the page is loading. Hence this minimal loading animation.
// src/components/loader/Loader.jsx
import './Loader.css';
const Loader = ({ loading, children }) => {
return loading ? (
<div className="loading">
<div className="effect-1 effects"></div>
<div className="effect-2 effects"></div>
<div className="effect-3 effects"></div>
</div>
) : (
children
);
};
export default Loader;
/* src/components/loader/Loader.css */
.loading {
position: absolute;
left: calc(50% - 35px);
top: 50%;
width: 55px;
height: 55px;
border-radius: 50%;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border: 3px solid transparent;
}
.loading .effect-1, .loading .effect-2 {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-left: 3px solid #7c251d;
border-radius: 50%;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.loading .effect-1 {
animation: rotate 1s ease infinite;
}
.loading .effect-2 {
animation: rotateOpacity 1s ease infinite 0.1s;
}
.loading .effect-3 {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-left: 3px solid #7c2d5f;
-webkit-animation: rotateOpacity 1s ease infinite 0.2s;
animation: rotateOpacity 1s ease infinite 0.2s;
border-radius: 50%;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.loading .effects {
transition: all 0.3s ease;
}
@keyframes rotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
@keyframes rotateOpacity {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
opacity: 0.1;
}
100% {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
opacity: 1;
}
}
Authenticate
Yes I build a separate component to check for the auth status and render/redirect accordingly.
// src/components/Auth.jsx
import Layout from "components/Layout";
import { AppContext } from "contexts/app";
import { useContext, useState, useEffect } from "react";
import { Redirect } from "react-router";
import Loader from "./loader/Loader";
const Authenticate = ({ children }) => {
const { session } = useContext(AppContext);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!session.loading) {
setLoading(false);
}
}, [session]);
if (loading) {
return (
<Layout>
<Loader loading={true}></Loader>
</Layout>
);
}
if (session.accessToken) {
return children;
} else {
return <Redirect to={"/login?then="+window.location.href.split("org/")[1]} />;
}
};
export default Authenticate;
This Authenticate
component is passed with the JSX component as an argument. If the session exists, it renders the component. Else it redirects to /login
Auth Button
// src/components/AuthButton.jsx
import { signInFirebase } from "apis/firebase";
const AuthButton = ({ onAuthSuccess, onAuthFailure }) => {
const handleClick = async () => {
const response = await signInFirebase();
const { status } = response;
if (status === "success") {
let data = {};
if (response.user) {
const { uid, email, displayName, accessToken } = response.user;
data = { uid, email, displayName, accessToken }
}
return onAuthSuccess(data);
}
return onAuthFailure(response);
};
return (
<button
className="btn px-5 text-white auth-btn"
onClick={handleClick}
>
Sign in
</button>
);
};
export default AuthButton;
The AuthButton
component takes in 2 functions as parameters: onAuthSuccess
and onAuthFailure
. These 2 functions should be defined and passed when using this component. If you can't understand what is that import { signInFirebase } from "api/firebase"
, then you might need to check the first part of this post
Profile
// src/components/Profile.jsx
const Profile = ({ userName = "", email ="", house="" }) => {
return (
<div className="user-profile text-center">
<h2 className="text-uppercase">{userName}</h2>
<p className="text-lowercase">{email}</p>
<h5 className="">{house}</h5>
</div>
)
}
export default Profile;
Creating Pages
Login Page
// src/pages/login.jsx
import { useContext } from "react";
import { Redirect } from "react-router";
import AuthButton from "components/AuthButton";
import Footer from "components/footer/Footer";
import { AppContext } from "contexts/app";
import logo from "assets/images/logo.png";
const LoginPage = () => {
const { session, setSession } = useContext(AppContext);
const handleAuthFailure = (response) => {
const { errorCode, errorMessage } = response;
alert(`${errorCode}: ${errorMessage}`);
};
const handleAuthSuccess = (response) => {
setSession(response);
};
if(session.accessToken) {
return <Redirect to={window.location.href.split("=")[1]} />;
}
return (
<main className="bg-color-maroon">
<div style={{ paddingTop: "1rem", minHeight: "75vh" }}>
<div className="container event-pass-page">
<div className="m-4 text-center">
<img src={logo} style={{ width:"20%", paddingBottom: "2rem" }} alt="treehouse banner" />
<h1 className="text-center text-white mb-4 heading text-uppercase">
Welcome to IITM BS Students Portal!!
</h1>
<p className="text-center text-white mb-5">
Please sign in with your IIT Madras Student Email ID to get started!
</p>
<AuthButton onAuthSuccess={handleAuthSuccess} onAuthFailure={handleAuthFailure} />
</div>
<h6 className="text-center my-5 mx-5" style={{ color: "rgba(255,255,255,0.6)" }}>
If you face any issues signing in with your student mail id, please let us know: <br />
<a href="mailto:webops@student.onlinedegree.iitm.ac.in" className="text-white">
Web Team
</a>
</h6>
</div>
</div>
<Footer />
</main>
);
};
export default LoginPage;
Login page, if you remember well, this page shows up when the user is not authenticated. There is the AuthButton
which is passed with 2 functions handleAuthSuccess
and handleAuthFailure
. The context data is parsed here, both the session
and setSession
and the setSession is used to set the response from sign in to the session.
Home Page
Home page is rather a simple one!
// src/pages/home.jsx
import Layout from "components/Layout";
import Container from "components/Container";
import electionsImg from "assets/images/elections.webp";
import { Link } from "react-router-dom";
const HomePage = () => {
return (
<Layout>
<Container bgColor="bg-color-maroon">
<img src={electionsImg} className="w-100" alt="header img" />
</Container>
<Container>
<p>It is an honor and a privilege for the IITM BS Degree Student Affairs and the Election Committee to organize the House Council elections. We hope to conduct fair and equal voting access that matches the best person to each of the House Council positions.</p>
<p>The IITM BS Degree program is structured in a dynamic manner with a plethora of activities around the curriculum. The cohort of our learners brings the best of the diversity of India and abroad. Even more, they are distributed across various age groups {"&"} subjects, adding more flavors to the mix. Within such a vibrant community, leadership is surely an opportunity and a challenge. </p>
</Container>
<Container bgColor="bg-color-maroon">
<p>The right leadership of Group Leaders, Secretaries, Deputy Secretaries and Web Administrators can ensure better activities and opportunities for all our students.</p>
<p>In this phase of the elections, we will be voting for the Secretary, Deputy Secretary and Web Admin for each of our twelve houses.</p>
</Container>
<Container>
<p>This website has been developed to provide you candidate information and a voting form that allows you to vote for the candidates of your house.</p>
<p>Please do share your suggestions and feedback as it helps us make our processes and systems even better.</p>
<div className="d-flex justify-content-center w-100">
<Link to="/candidates" className="btn auth-btn px-4 text-white">Know your Candidates</Link>
</div>
</Container>
</Layout>
);
};
export default HomePage;
Candidates Page
Candidates page shows the list of candidates from the student's house who are standing for the election. We already have functions created to fetch candidates from the database. All we need to do here is call those functions
// src/pages/candidates.jsx
const [sec, setSec] = useState([]);
const [dySec, setDySec] = useState([]);
const [webAd, setWebAd] = useState([]);
useEffect(() => {
getElectionCandidates().then((r={}) => {
setHouse(r.house);
setSec(r.sec);
setDySec(r.dysec);
setWebAd(r.webad);
}).then(() => {
setLoading(false)
})
}, [])
Displaying details is the next headache. The descriptions were both short and long. Hence I contained it in a Model
const [modal, showModal] = useState(false);
const closeModal = () => showModal(false)
const openModal = (name, email, photo, intro, doc) => {
showModal(true);
setTimeout(() => {
document.querySelector('.btn-close').classList.add('bg-light')
document.querySelector(".candidate-name").innerText = name;
document.querySelector(".candidate-desc").innerText = intro;
document.querySelector(".candidate-mail").innerText = email
document.querySelector(".candidate-doc").href = "https://"+doc.split("https://")[1];
document.querySelector(".candidate-pic").src = photo;
}, 1);
}
<Modal show={modal} onHide={closeModal} size="lg" centered>
<Modal.Header className="bg-color-maroon text-color-gold" closeButton>
<Modal.Title>
<h4 className="candidate-name"></h4>
</Modal.Title>
</Modal.Header>
<Modal.Body className="p-0 m-0">
<Row className="m-0 p-0">
<Col xs={12} md={4} className="p-0">
<img src="" className="candidate-pic h-100 w-100" alt="modal img" />
</Col>
<Col xs={12} md={8} className="d-flex align-items-center">
<div className="m-2">
<p className="candidate-mail font-italic small"></p>
<p className="candidate-desc mt-1"></p>
</div>
</Col>
</Row>
</Modal.Body>
<Modal.Footer>
<a href="#modal" target={"_blank"} className="candidate-doc btn btn-outlined rounded">
Know More
</a>
</Modal.Footer>
</Modal>
Rendering the data in the page
<Container>
<img src={candidateImg} width="100%" alt="candidate" />
<h3 className="text-center mb-4">Secretary</h3>
{sec.length === 0 ? (
<h6 className="text-center">No nominations recieved for the post of Secretary</h6>
) : (
<div className="d-flex flex-wrap justify-content-center">
{sec.map((candidate) => {
return (
<div
className="card m-1"
key={candidate.email}
style={{
width:"300px",
height:"300px",
cursor:"pointer",
backgroundImage: `url("https://drive.google.com/uc?export=view&id=${candidate.photo.split("=")[1]}")`,
backgroundSize:"cover",
backgroundPosition: "center center"
}}
onClick={()=>openModal(
candidate.name,
candidate.email,
`https://drive.google.com/uc?export=view&id=${candidate.photo.split("=")[1]}`,
candidate.intro,
candidate.doc
)}
>
<div className="text-white position-absolute w-100 pt-5 px-2" style={{bottom:'0', background:`linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.8))`}}>
<h4>{candidate.name}</h4>
<p className="small">{candidate.email}</p>
</div>
</div>
)
})}
</div>
)}
</Container>
This template is followed for both deputy secretary and web admin too.
Displaying details is the next headache. The descriptions were both short and long. Hence I contained it in a Model
Vote page
This is the important page for this website. A single mistake in this page would make this project useless. (Well there were lots of issues after getting this project to production. I spent an entire night only to find that the issue was because of replacing 'a' with 'e' in Sundarbans)
First, I needed a timer. I can't keep track of time to open and close the election portal. Hence I created a timer along with a clock to be rendered on the website.
// src/pages/vote.jsx
const currDate = new Date();
const startDate = new Date(2022, 8, 2, 0, 0, 0);
const endDate = new Date(2022, 8, 4, 0, 0, 0);
const [time, setTime] = useState(0)
setInterval(() => {
const endTime = endDate.getTime();
const currTime = new Date().getTime();
setTime(endTime-currTime);
}, 1000)
<h6 className="text-center">Voting Closes in {String(parseInt(time/(1000*60*60))).length===1 ? "0"+String(parseInt(time/(1000*60*60))) : String(parseInt(time/(1000*60*60)))}:{String(parseInt((time%(1000*60*60))/(1000*60))).length===1 ? "0"+String(parseInt((time%(1000*60*60))/(1000*60))) : String(parseInt((time%(1000*60*60))/(1000*60)))}:{String(parseInt((time%(1000*60))/(1000))).length===1 ? "0"+String(parseInt((time%(1000*60))/(1000))) : String(parseInt((time%(1000*60))/(1000)))}</h6>
{startDate<currDate && endDate>currDate ? (} : ()}
I know the timer part is a bit of a mess 😅
Next, the selection of candidates and store them
const [data, setData] = useState({sec: [], dysec: [], webad: [], mentor: []});
const [resp, setResp] = useState({sec: "None Selected",dysec: "None Selected",webad: "None Selected",house: "",email: ""})
{data.sec.length === 0 ? (
<h6>No nominations recieved for the post of Web Admin</h6>
) : (
<div className="d-flex flex-wrap justify-content-center">
{data.sec.map((candidate) => {
return (
<div className="card m-1" style={{width:"300px",height:"300px"}} key={candidate.email}>
<input type="radio" className="btn-check" name="sec" id={candidate.email+"sec"} required />
<label className="btn card p-0 h-100 w-100" style={{backgroundImage:`url(${"https://drive.google.com/uc?export=view&id="+candidate.photo.split("=")[1]})`,backgroundSize:"cover",backgroundPosition:"center center"}} htmlFor={candidate.email+"sec"} onClick={()=>setResp({...resp, sec: `${candidate.email} - ${candidate.name}`})}>
<div className="card-body pb-0 text-start position-absolute w-100 pt-5" style={{bottom:"0",background:`linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.8))`}}>
<h5 className="card-title">{candidate.name}</h5>
<p className="small">{candidate.email}</p>
</div>
</label>
</div>
)
})}
</div>
)}
Next the storing of votes 🤯
const handleSubmit = () => {
resp.house = data.house;
resp.email = data.email;
if (window.confirm(`Confirm the Candidates:\nSecretary: ${resp.sec.split("-")[1] || "No Nominations"} - ${resp.sec.split("@")[0]}\nDeputy Secretary: ${resp.dysec.split("-")[1] || "No Nominations"} - ${resp.dysec.split("@")[0]}\nWeb Admin: ${resp.webad.split("-")[1] || "No Nominations"} - ${resp.webad.split("@")[0]}`)) {
document.getElementById('vote-form').innerHTML='<div class="loading"><div class="effect-1 effects"></div><div class="effect-2 effects"></div><div class="effect-3 effects"></div></div>'
updateVote(resp).then((r) => {
document.getElementById('vote-form').innerHTML = `
<div class="row justify-content-center w-100 align-items-center" style="height:'70vh'">
<h5 class='text-center mb-3'>Thanks for casting your vote</h5>
<div>
<h5>Secretary: </h5>
<p>${resp.sec || "No nominations"}</p>
<h5>Deputy Secretary</h5>
<p>${resp.dysec || "No nominations"}</p>
<h5>Web Admin</h5>
<p>${resp.webad || "No nominations"}</p>
</div>
<a class="btn auth-btn mt-3 col-6 text-white" href="/">Go to Home</a>
</div>
`
})
}
}
Creating Routes
Last part 😍
Combining everything
Routing is the main part of any multi page website. I used react-router@5.2.0
when I developed this website. (This version might be deprecateed by now)
import HomePage from "pages/home";
import ElectionsPage from "pages/elections";
import LoginPage from "pages/login";
import ProfilePage from "pages/profile";
import Authenticate from "components/Auth";
import CandidatesPage from "pages/canididates";
import VotePage from "pages/vote";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import { getAuth, onAuthStateChanged } from "firebase/auth";
<AppContext.Provider value={{ session, setSession }}>
<BrowserRouter>
<Switch>
<Route
exact
path="/elections"
render={(routeProps) => (
<Authenticate>
<ElectionsPage {...routeProps} />
</Authenticate>
)}
/>
<Route exact path="/login" render={(routeProps) => <LoginPage {...routeProps} />} />
<Route
exact
path="/profile"
render={(routeProps) => (
<Authenticate>
<ProfilePage {...routeProps} />
</Authenticate>
)}
/>
<Route
exact
path="/candidates"
render={(routeProps) => (
<Authenticate>
<CandidatesPage {...routeProps} />
</Authenticate>
)}
/>
<Route
exact
path="/vote"
render={(routeProps) => (
<Authenticate>
<ElectionsPage {...routeProps} />
</Authenticate>
)}
/>
<Route
exact
path="/"
render={(routeProps) => (
<Authenticate>
<HomePage {...routeProps} />
</Authenticate>
)}
/>
<Redirect from="*" to="/" />
</Switch>
</BrowserRouter>
</AppContext.Provider>
In the routes, I didn't use Authenticate
component for /login
. Because if the user is not logged in, it'll redirect to /login
then again the same process is repeated resulting in an endless loop of multiple renders.
Wrap up
That's all from this project. There are still a few things which you guys can search for. You can implement blockchain technologies for casting vites. There are more obvious technologies out there and I would love to hear them in the comments~