Project Structure
The updated dependency in package.json file of frontend will look like:
"dependencies": {
"@reduxjs/toolkit": "^2.2.1",
"axios": "^1.6.7",
"nodemon": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
Example: Create the required files and write the following code.
CSS
/* App.css */ @import url ( 'https://fonts.googleapis.com/css2?family=Poppins&display=swap' ); :root{ --primary- color : #0DFF92 ; --dark- color : #222222 ; --light- color : #f0f0f0 ; } body, html{ height : 100% ; background : var(--dark-color) } * > *{ font-family : 'Poppins' , sans-serif ; } .container{ display : block ; position : relative ; margin : 40px auto ; height : auto ; width : 800px ; padding : 20px ; } .container .title{ font-size : 3em ; text-align : center ; border : 5px solid var(--primary-color); padding : . 3em . 2em ; border-radius: 4px ; } .text-light { color : var(--light-color) } .container ul{ list-style : none ; margin : 0 ; padding : 0 ; overflow : auto ; } .container .questions{ padding : 3em ; } /* .container .questions .qid{ padding: .2em .7em; color: #222222; background-color: #0DFF92; border-radius: 50px; } */ .container .grid{ margin-top : 3em ; display : grid; grid-template-columns: 1 fr 1 fr; } .container .btn{ padding : . 2em 1.7em ; border : none ; border-radius: . 1em ; font-size : 1.2em ; } .container .btn:hover{ cursor : pointer ; background-color : #f0f0f0 ; color : #202020 ; } .next{ background-color : var(--primary-color); justify-self: flex-end; } .prev{ background-color : #faff5a ; justify-self: flex-start; } ul li{ color : #AAAAAA ; display : block ; position : relative ; float : left ; width : 100% ; height : 100px ; border-bottom : 1px solid #333 ; } ul li input[type=radio]{ position : absolute ; visibility : hidden ; } ul li label{ display : block ; position : relative ; font-weight : 300 ; font-size : 1.35em ; padding : 25px 25px 25px 80px ; margin : 10px auto ; height : 30px ; z-index : 9 ; cursor : pointer ; -webkit-transition: all 0.25 s linear; } ul li:hover label{ color : #FFFFFF ; } ul li .check{ display : block ; position : absolute ; border : 5px solid #AAAAAA ; border-radius: 100% ; height : 25px ; width : 25px ; top : 30px ; left : 20px ; z-index : 5 ; transition: border . 25 s linear; -webkit-transition: border . 25 s linear; } ul li:hover .checked { border : 5px solid #FFFFFF ; } ul li .check::before { display : block ; position : absolute ; content : '' ; border-radius: 100% ; height : 15px ; width : 15px ; top : 5px ; left : 5px ; margin : auto ; transition: background 0.25 s linear; -webkit-transition: background 0.25 s linear; } input[type=radio]:checked ~ .check { border : 5px solid var(--primary-color) } input[type=radio]:checked ~ .check::before{ background : var(--primary-color) } input[type=radio]:checked ~ .text-primary{ color : var(--primary-color) } /* To get selected option we are using this checked class */ .checked { border : 5px solid var(--primary-color) !important ; } .checked::before{ background : var(--primary-color) } |
CSS
/* index.css */ body { margin : 0 ; font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , 'Roboto' , 'Oxygen' , 'Ubuntu' , 'Cantarell' , 'Fira Sans' , 'Droid Sans' , 'Helvetica Neue' , sans-serif ; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family : source-code-pro, Menlo, Monaco, Consolas, 'Courier New' , monospace ; } |
CSS
/* Main.css */ .container ol li { font-size : 1.4em ; color : #cecece ; } .start { display : flex; justify- content : center ; padding-top : 2em ; } .start .btn { padding : . 2em 1.7em ; border : none ; border-radius: . 1em ; font-size : 1.2em ; color : #202020 ; text-decoration : none ; background-color : #faff5a ; } #form { display : flex; justify- content : center ; margin-top : 4em ; } #form .userid { padding : . 7em 2em ; width : 50% ; border : none ; border-radius: 3px ; font-size : 1em ; } |
CSS
/* Result.css */ .flex- center { display : flex; justify- content : center ; flex- direction : column; border : 1px solid #cecece ; padding : 3em 4em ; gap: 1em ; } .container .flex { display : flex; justify- content : space-between; } .container .flex span { font-size : 1.4em ; color : #cecece ; } .container .flex span.achive { font-weight : bold ; color : #ff2a66 ; color : #2aff95 ; } table { width : 100% ; } .table-header { color : #cecece ; font-size : 1.1em ; text-align : center ; background : #212121 ; padding : 18px 0 ; } .table-body { font-size : 1.1em ; text-align : center ; /* color: #cecece; */ background : #d8d8d8 ; padding : 18px 0 ; } .table-header>tr>td { border : 1px solid #faff5a ; } |
Javascript
// index.js import React from 'react' ; import ReactDOM from 'react-dom/client' ; import './styles/index.css' ; import App from './components/App' ; /** Redux Store */ import store from './redux/store' ; import { Provider } from 'react-redux' ; const root = ReactDOM.createRoot(document.getElementById( 'root' )); root.render( <Provider store={store}> <App /> </Provider> ); |
Javascript
// Result.js import React, { useEffect } from 'react' import '../styles/Result.css' ; import { Link } from 'react-router-dom' ; import ResultTable from './ResultTable' ; import { useDispatch, useSelector } from 'react-redux' ; import { attempts_Number, earnPoints_Number, flagResult } from '../helper/helper' ; /** import actions */ import { resetAllAction } from '../redux/question_reducer' ; import { resetResultAction } from '../redux/result_reducer' ; import { usePublishResult } from '../hooks/setResult' ; export default function Result() { const dispatch = useDispatch() const { questions: { queue, answers }, result: { result, userId } } = useSelector(state => state) const totalPoints = queue.length * 10; const attempts = attempts_Number(result); const earnPoints = earnPoints_Number(result, answers, 10) const flag = flagResult(totalPoints, earnPoints) /** store user result */ usePublishResult({ result, username: userId, attempts, points: earnPoints, achived: flag ? "Passed" : "Failed" }); function onRestart() { dispatch(resetAllAction()) dispatch(resetResultAction()) } return ( <div className= 'container' > <h1 className= 'title text-light' >Quiz Application</h1> <div className= 'result flex-center' > <div className= 'flex' > <span>Username</span> <span className= 'bold' >{userId || "" }</span> </div> <div className= 'flex' > <span>Total Quiz Points : </span> <span className= 'bold' >{totalPoints || 0}</span> </div> <div className= 'flex' > <span>Total Questions : </span> <span className= 'bold' >{queue.length || 0}</span> </div> <div className= 'flex' > <span>Total Attempts : </span> <span className= 'bold' >{attempts || 0}</span> </div> <div className= 'flex' > <span>Total Earn Points : </span> <span className= 'bold' >{earnPoints || 0}</span> </div> <div className= 'flex' > <span>Quiz Result</span> <span style={{ color: `${flag ? "#2aff95" : "#ff2a66" }` }} className= 'bold' >{flag ? "Passed" : "Failed" }</span> </div> </div> <div className= "start" > <Link className= 'btn' to={ '/' } onClick={onRestart}>Restart</Link> </div> <div className= "container" > { /* result table */ } <ResultTable></ResultTable> </div> </div> ) } |
Javascript
// Questions.js import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' /** Custom Hook */ import { useFetchQestion } from '../hooks/FetchQuestion' import { updateResult } from '../hooks/setResult' export default function Questions({ onChecked }) { const [checked, setChecked] = useState(undefined) const { trace } = useSelector(state => state.questions); const result = useSelector(state => state.result.result); const [{ isLoading, apiData, serverError }] = useFetchQestion() const questions = useSelector(state => state.questions.queue[state.questions.trace]) const dispatch = useDispatch() useEffect(() => { dispatch(updateResult({ trace, checked })) }, [checked]) function onSelect(i) { onChecked(i) setChecked(i) dispatch(updateResult({ trace, checked })) } if (isLoading) return <h3 className= 'text-light' >isLoading</h3> if (serverError) return <h3 className= 'text-light' > {serverError || "Unknown Error" } </h3> return ( <div className= 'questions' > <h2 className= 'text-light' >{questions?.question}</h2> <ul key={questions?.id}> { questions?.options.map((q, i) => ( <li key={i}> <input type= "radio" value={ false } name= "options" id={`q${i}-option`} onChange={() => onSelect(i)} /> <label className= 'text-primary' htmlFor={`q${i}-option`}>{q}</label> <div className={ `check ${result[trace] == i ? 'checked' : '' }`}> </div> </li> )) } </ul> </div> ) } |
Javascript
// ResultTable.js import React, { useEffect, useState } from 'react' import { getServerData } from '../helper/helper' export default function ResultTable() { const [data, setData] = useState([]) useEffect(() => { getServerData(` ${process.env.REACT_APP_SERVER_HOSTNAME}/api/result`, (res) => { setData(res) }) }) return ( <div> <table> <thead className= 'table-header' > <tr className= 'table-row' > <td>Name</td> <td>Attemps</td> <td>Earn Points</td> <td>Result</td> </tr> </thead> <tbody> {!data ?? <div>No Data Found </div>} { data.map((v, i) => ( <tr className= 'table-body' key={i}> <td>{v?.username || '' }</td> <td>{v?.attempts || 0}</td> <td>{v?.points || 0}</td> <td>{v?.achived || "" }</td> </tr> )) } </tbody> </table> </div> ) } |
Javascript
// helper.js import { useSelector } from "react-redux" ; import { Navigate } from "react-router-dom" ; import axios from 'axios' export function attempts_Number(result) { return result.filter(r => r !== undefined).length; } export function earnPoints_Number(result, answers, point) { return result.map((element, i) => answers[i] === element).filter(i => i).map(i => point).reduce( (prev, curr) => prev + curr, 0); } export function flagResult(totalPoints, earnPoints) { return (totalPoints * 50 / 100) < earnPoints; /** earn 50% marks */ } /** check user auth */ export function CheckUserExist({ children }) { const auth = useSelector(state => state.result.userId) return auth ? children : <Navigate to={ '/' } replace={ true }></Navigate> } /** get server data */ export async function getServerData(url, callback) { const data = await (await axios.get(url))?.data; return callback ? callback(data) : data; } /** post server data */ export async function postServerData(url, result, callback) { const data = await (await axios.post(url, result))?.data; return callback ? callback(data) : data; } |
Javascript
// FetchQuestion.js import { useEffect, useState } from "react" import { useDispatch } from "react-redux" ; import { getServerData } from "../helper/helper" ; /** redux actions */ import * as Action from '../redux/question_reducer' /* fetch question hook to fetch api data and set value to store */ export const useFetchQestion = () => { const dispatch = useDispatch(); const [getData, setGetData] = useState({ isLoading: false , apiData: [], serverError: null }); useEffect(() => { setGetData(prev => ({ ...prev, isLoading: true })); /** async function fetch backend data */ (async () => { try { const [{ questions, answers }] = await getServerData(` ${process.env.REACT_APP_SERVER_HOSTNAME}/api/questions`, (data) => data) if (questions.length > 0) { setGetData(prev => ({ ...prev, isLoading: false })); setGetData(prev => ({ ...prev, apiData: questions })); /** dispatch an action */ dispatch(Action.startExamAction({ question: questions, answers })) } else { throw new Error( "No Question Avalibale" ); } } catch (error) { setGetData(prev => ({ ...prev, isLoading: false })); setGetData(prev => ({ ...prev, serverError: error })); } })(); }, [dispatch]); return [getData, setGetData]; } /** MoveAction Dispatch function */ export const MoveNextQuestion = () => async (dispatch) => { try { dispatch(Action.moveNextAction()); /** increase trace by 1 */ } catch (error) { console.log(error) } } /** PrevAction Dispatch function */ export const MovePrevQuestion = () => async (dispatch) => { try { dispatch(Action.movePrevAction()); /** decrease trace by 1 */ } catch (error) { console.log(error) } } |
Javascript
import { postServerData } from '../helper/helper' import * as Action from '../redux/result_reducer' export const PushAnswer = (result) => async (dispatch) => { try { await dispatch(Action.pushResultAction(result)) } catch (error) { console.log(error) } } export const updateResult = (index) => async (dispatch) => { try { dispatch(Action.updateResultAction(index)); } catch (error) { console.log(error) } } /** insert user data */ export const usePublishResult = (resultData) => { const { result, username } = resultData; (async () => { try { if (result !== [] && !username) throw new Error( "Couldn't get Result" ); await postServerData(`http: //localhost:${process.env.PORT}/api/result`, resultData, data => data) } catch (error) { console.log(error) } })(); } |
Javascript
// question_reducer.js import { createSlice } from "@reduxjs/toolkit" ; /** create reducer */ export const questionReducer = createSlice({ name: 'questions' , initialState: { queue: [], answers: [], trace: 0 }, reducers: { startExamAction: (state, action) => { let { question, answers } = action.payload return { ...state, queue: question, answers } }, moveNextAction: (state) => { return { ...state, trace: state.trace + 1 } }, movePrevAction: (state) => { return { ...state, trace: state.trace - 1 } }, resetAllAction: () => { return { queue: [], answers: [], trace: 0 } } } }) export const { startExamAction, moveNextAction, movePrevAction, resetAllAction } = questionReducer.actions; export default questionReducer.reducer; |
Javascript
// result_reducer.js import { createSlice } from "@reduxjs/toolkit" export const resultReducer = createSlice({ name: 'result' , initialState: { userId: null , result: [] }, reducers: { setUserId: (state, action) => { state.userId = action.payload }, pushResultAction: (state, action) => { state.result.push(action.payload) }, updateResultAction: (state, action) => { const { trace, checked } = action.payload; state.result.fill(checked, trace, trace + 1) }, resetResultAction: () => { return { userId: null , result: [] } } } }) export const { setUserId, pushResultAction, resetResultAction, updateResultAction } = resultReducer.actions; export default resultReducer.reducer; |
Javascript
// store.js import { combineReducers, configureStore } from '@reduxjs/toolkit' ; /** call reducers */ import questionReducer from './question_reducer' ; import resultReducer from './result_reducer' ; const rootReducer = combineReducers({ questions: questionReducer, result: resultReducer }) /** create store with reducer */ export default configureStore({ reducer: rootReducer }); |
Javascript
import './App.css' ; import { createBrowserRouter, RouterProvider } from 'react-router-dom' /** import components */ import Main from './Main.js' ; import Quiz from './Quiz.js' ; import Result from './Result.js' ; import { CheckUserExist } from '../src/helper/' ; /** react routes */ const router = createBrowserRouter([ { path: '/' , element: <Main></Main> }, { path: '/quiz' , element: <CheckUserExist><Quiz /></CheckUserExist> }, { path: '/result' , element: <CheckUserExist><Result /></CheckUserExist> }, ]) function App() { return ( <> <RouterProvider router={router} /> </> ); } export default App; |
Quiz App using MERN Stack
In this article, we’ll walk through the step-by-step process of creating a complete quiz application with MongoDB, ReactJS, ExpressJS, and NodeJS. This application will provide users with a user-friendly interface for taking and submitting quizzes and a scoreboard to check their standing among others.