Build a quiz App with Next.js and TailwindCSS!

Build a quiz App with Next.js and TailwindCSS!

Featured on daily.dev
Avneesh Agarwal

Published on Nov 7, 2021

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Hey guys, this is gonna be a tutorial plus a challenge. I also have a giveaway at the end so stay tuned ;)

Demo

You can try it out yourself here- quiz-challenge.vercel.app

Setting up

Creating a Next app with TailwindCSS

I am going to use tailwind for the basic stylings needed in the app

npx create-next-app next-stripe-demo -e with-tailwindcss

Cleanup

Delete everything in pages/index.js after the Head

import Head from "next/head";


export default function Home() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen py-2">
      <Head>
        <title>Quiz App</title>
      </Head>
    </div>
  )
}

Starting the app

npm run dev # npm
yarn dev # yarn

Create a few questions

We are going to use the questions from a JSON array, so create a questions.json file in the root of the directory. The questions array should look like this-

[
  {
    "question": "What type of framework is Next.js?",
    "answerOptions": [
      { "answer": "Frontend" },
      { "answer": "Backend" },
      { "answer": "FullStack", "isCorrect": true },
      { "answer": "None of the above" }
    ]
  },
  {
    "question": "When was Next.js released?",
    "answerOptions": [
      { "answer": "20 September 2019" },
      { "answer": "14 January 2017" },
      { "answer": "25 October 2016", "isCorrect": true },
      { "answer": "28 March 2018" }
    ]
  },
  {
    "question": "Which CSS Framework are we using?",
    "answerOptions": [
      { "answer": "Bootstrap" },
      { "answer": "TailwindCSS", "isCorrect": true },
      { "answer": "Chakra UI" },
      { "answer": "Bulma CSS" }
    ]
  },
  {
    "question": "Which class in Tailwind is used to set flex direction of column?",
    "answerOptions": [
      { "answer": "col" },
      { "answer": "col-flex" },
      { "answer": "flex-col", "isCorrect": true },
      { "answer": "None of the above" }
    ]
  }
]

Creating the UI for the Quiz

Our quiz will look like this- image.png

Styling the container of our app.

I will add the following styles to the div containing the app-

 <div className="flex flex-col w-screen px-5 h-screen bg-[#1A1A1A] justify-center items-center">

This will give us a blank screen with the background color- #1A1A1A.

Question section

We are going to hard code the values for now.

<div className="flex flex-col items-start w-full">
  <h4 className="mt-10 text-xl text-white/60">Question 1 of 5</h4>
  <div className="mt-4 text-2xl text-white">
    What type of framework is Next.js?
  </div>
</div>

Now our app is looking like this

image.png

Creating the answers We are going to map through the answers the first question, to show the options.

<div className="flex flex-col w-full">
  {questions[0].answerOptions.map((answer, index) => (
    <div
      key={index}
      className="flex items-center w-full py-4 pl-5 m-2 ml-0 space-x-2 border-2 cursor-pointer bg-white/5 border-white/10 rounded-xl"
    >
      <input type="radio" className="w-6 h-6 bg-black" />
      <p className="ml-6 text-white">{answer.answer}</p>
    </div>
  ))}
</div>

we also need to import questions from the questions.json file, so add this import line-

import questions from "../questions.json";

It will now give us all the options with a radio button-

image.png

The radio button doesn't go well with our theme, so I am going to add some custom styles for it in globals.css, so follow the instructions-

  • Create a styles folder and globals.css file inside it

Untitled.png

  • Inside globals.css add the following-
@tailwind base;
@tailwind components;
@tailwind utilities;
  • Import globals.css instead of tailwindcss/tailwind.css in _app.js
import "../styles/globals.css";
  • Add the styles for the custom radio button
input[type="radio"]:after {
  width: 24px;
  height: 24px;
  border-radius: 24px;
  cursor: pointer;
  position: relative;
  background-color: #535353;
  content: "";
  display: inline-block;
  visibility: visible;
  border: 2px solid white;
}

input[type="radio"]:checked:after {
  width: 24px;
  height: 24px;
  border-radius: 24px;
  cursor: pointer;
  position: relative;
  background-color: #4F46E5;
  content: "";
  display: inline-block;
  visibility: visible;
  border: 2px solid white;
}

Now it gives us a better radio button that matches the theme like this-

image.png

Adding the buttons to navigate through the questions

<div className="flex justify-between w-full mt-4 text-white">
  <button className="w-[49%] py-3 bg-indigo-600 rounded-lg">Previous</button>
  <button className="w-[49%] py-3 bg-indigo-600 rounded-lg">Next</button>
</div>

This gives us the buttons for navigating as follows.

image.png

With this, we are done setting up the UI.

Adding the logic for our quiz

Navigation We are first going to build the navigation functionality

Create a state for the current question-

const [currentQuestion, setCurrentQuestion] = useState(0);

Create 2 functions to handle Next and Previous-

const handlePrevious = () => {
  const prevQues = currentQuestion - 1;
  prevQues >= 0 && setCurrentQuestion(prevQues);
};

const handleNext = () => {
  const nextQues = currentQuestion + 1;
  nextQues < questions.length && setCurrentQuestion(nextQues);
};

Assigning the functions to the respective buttons

  <button
    onClick={handlePrevious}
    className="w-[49%] py-3 bg-indigo-600 rounded-lg"
  >
    Previous
  </button>
  <button
    onClick={handleNext}
    className="w-[49%] py-3 bg-indigo-600 rounded-lg"
  >
    Next
  </button>

Remove the hardcoded values for the question-

<div className="flex flex-col items-start w-full">
  <h4 className="mt-10 text-xl text-white/60">
    Question {currentQuestion + 1} of {questions.length}
  </h4>
  <div className="mt-4 text-2xl text-white">
    {questions[currentQuestion].question}
  </div>
</div>

Map the answers for the current question instead of the first question-

questions[currentQuestion].answerOptions.map

Now we can easily move through the questions 🎉

Ability to select options

Create a state to hold all the selected answers-

const [selectedOptions, setSelectedOptions] = useState([]);

We will now make a function to set the selected option-

const handleAnswerOption = (answer) => {
  setSelectedOptions([
    (selectedOptions[currentQuestion] = { answerByUser: answer }),
  ]);
  setSelectedOptions([...selectedOptions]);
};

Now, we need to trigger in onClick of the option and check the radio button-

{questions[currentQuestion].answerOptions.map((answer, index) => (
    <div
      key={index}
      className="flex items-center w-full py-4 pl-5 m-2 ml-0 space-x-2 border-2 cursor-pointer border-white/10 rounded-xl bg-white/5"
      onClick={(e) => handleAnswerOption(answer.answer)}
    >
      <input
        type="radio"
        name={answer.answer}
        value={answer.answer}
        onChange={(e) => handleAnswerOption(answer.answer)}
        checked={
          answer.answer === selectedOptions[currentQuestion]?.answerByUser
        }
        className="w-6 h-6 bg-black"
      />
      <p className="ml-6 text-white">{answer.answer}</p>
    </div>
  ));
}

Now if you select an option then it will be stored as an object in the selectedOptions state. To check this let's console.log selectedOptions in handleAnswerOption-

const handleAnswerOption = (answer) => {
  setSelectedOptions([
    (selectedOptions[currentQuestion] = { answerByUser: answer }),
  ]);
  setSelectedOptions([...selectedOptions]);
  console.log(selectedOptions);
};

After clicking the options, it will show an array of options selected like this-

image.png

Calculating and showing the score

Make 2 states, one to store the score and the other to see if we need to show the score or not-

const [score, setScore] = useState(0);
const [showScore, setShowScore] = useState(false);

Now we need to create a new function that calculates the score based on the answers-

const handleSubmitButton = () => {
  let newScore = 0;
  for (let i = 0; i < questions.length; i++) {
    questions[i].answerOptions.map(
      (answer) =>
        answer.isCorrect &&
        answer.answer === selectedOptions[i]?.answerByUser &&
        (newScore += 1)
    );
  }
  setScore(newScore);
  setShowScore(true);
};

Show submit button instead of next on last question

In the last question, we will need to show submit instead of next and run the handleSubmitButton function.

<button
  onClick={
    currentQuestion + 1 === questions.length ? handleSubmitButton : handleNext
  }
  className="w-[49%] py-3 bg-indigo-600 rounded-lg"
>
  {currentQuestion + 1 === questions.length ? "Submit" : "Next"}
</button>

Now if we submit then nothing really happens, so after we submit we should be able to see a screen like this-

image.png

To do this we are going to render the page based on showScore's value like this-

{showScore ? (
    <h1 className="text-3xl font-semibold text-center text-white">
      You scored {score} out of {questions.length}
    </h1>
  ) : (
    <>
      <div className="flex flex-col items-start w-full">
        <h4 className="mt-10 text-xl text-white/60">
          Question {currentQuestion + 1} of {questions.length}
        </h4>
        <div className="mt-4 text-2xl text-white">
          {questions[currentQuestion].question}
        </div>
      </div>
      <div className="flex flex-col w-full">
        {questions[currentQuestion].answerOptions.map((answer, index) => (
          <div
            key={index}
            className="flex items-center w-full py-4 pl-5 m-2 ml-0 space-x-2 border-2 cursor-pointer border-white/10 rounded-xl bg-white/5"
            onClick={(e) => handleAnswerOption(answer.answer)}
          >
            <input
              type="radio"
              name={answer.answer}
              value={answer.answer}
              checked={
                answer.answer === selectedOptions[currentQuestion]?.answerByUser
              }
              onChange={(e) => handleAnswerOption(answer.answer)}
              className="w-6 h-6 bg-black"
            />
            <p className="ml-6 text-white">{answer.answer}</p>
          </div>
        ))}
      </div>
      <div className="flex justify-between w-full mt-4 text-white">
        <button
          onClick={handlePrevious}
          className="w-[49%] py-3 bg-indigo-600 rounded-lg"
        >
          Previous
        </button>
        <button
          onClick={
            currentQuestion + 1 === questions.length
              ? handleSubmitButton
              : handleNext
          }
          className="w-[49%] py-3 bg-indigo-600 rounded-lg"
        >
          {currentQuestion + 1 === questions.length ? "Submit" : "Next"}
        </button>
      </div>
    </>
  );
}

Now our app is working completely fine 🥳

Giveaway

The winner gets the React and ServerLess Course course by James Q Quick

To participate in this giveaway

  • Make this quiz app better
  • Share it on your social media with the hashtag- next-quiz-challenge and don't forget to tag me :)

Important Dates

  • 18th November 2021: Submit your projects before 18th November 12 PM IST.
  • 20th November 2021: The winner will be announced on my social media.

Few features you can add-

  • Leaderboard
  • Show correct and incorrect answers
  • Timer
  • Improve UI

GitHub repository

Demo

All socials

Did you find this article valuable?

Support Avneesh Agarwal by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this