Build a Simple LMS-Integrated Quiz App Using Python, React.js, and LTI 1.3 – Part 3

Requirements for this part

  • Install the lastest Node.js for your OS: https://nodejs.org
  • A directory to contain a new ‘frontend’ project.

Question Data Structure and Initial Testing

In the next part, we will be building out the front-end. The super-simple quiz that we’ll build in React takes a JSON-formatted set of questions and a list of answers. Each answer will have an ‘isCorrect’ boolean property that will help us determine how to process the final score. The layout will resemble the following, with ‘questionText’ and an array of 4 ‘answerOptions’ objects with ‘answerText’ and ‘isCorrect’ properties.

   [
      {
        questionText: "Where is the Taj Mahal?",
        answerOptions: [
          { answerText: "Agra", isCorrect: true },
          { answerText: "New Delhi", isCorrect: false },
          { answerText: "Mumbai", isCorrect:: false },
          { answerText: "Chennai", isCorrect: false },
        ],
      },
      {
        questionText: "Where is the Great Wall of China?",
        answerOptions: [
          { answerText: "China", isCorrect: true },
          { answerText: "Beijing", isCorrect: false },
          { answerText: "Shanghai", isCorrect: false },
          { answerText: "Tianjin", isCorrect: false },
        ],
      },
    ]

The goal is to load up the data into a database, converting row objects into dictionary format, and serving up the entire payload through an API using our Flask server. Since we’re not quite there yet, and we want to focus on just the front-end at the moment, we’ll simply load this JSON into a React useEffect() hook for testing purposes.

Project Directory Structure

Feel free to change or add directories into a structure that’s more beneficial for your project, keeping everything more organized than what I have, and perhaps defining both a backend and frontend directory structure. The current layout looks like the following for our backend project — so far:

├── app.py
├── configs
│   ├── private.key
│   ├── public.key
│   └── reactquiz.json
├── env

I personally chose to start the frontend in a completely new project, where I then build the static files from the react.js end of things and copy over the final static files into a frontend directory, sitting at the root of the backend project. Like this:

├── app.py
├── configs
│   ├── private.key
│   ├── public.key
│   └── reactquiz.json
├── env
├── frontend
│   ├── static
│   └── templates

This is not scalable in the long term for large projects, unless you choose to automate the deployment of the static builds from your react application in some way or leverage updates to webpack. Since tooling for production builds is out of the scope of Part 3, we’ll start a second (create-react-app) project and perform a manual copy of built files to the Flask server for the final demonstration and testing.

Alright, let’s continue!

Wherever you choose to start your frontend piece (either in the current project root directory or separate project altogether), run the following at the terminal and keep in mind that this will create a new directory for the project.

npx create-react-app frontend

More information about create-react-app can be found here: https://create-react-app.dev/docs/getting-started/

After creating the app, follow the directions that are given to change to the new directory and test that everything is working:

cd frontend
npm start

Stop the development server (Ctrl-C ). Open your editor in the frontend directory and remove everything from the src subdirectory. We’re going to create a few new files as we go along.

Tailwind – Note: Tailwind is a rapidly changing project. Make sure you reference the current install docs if you run into any issues with the steps below.

Back to the terminal and the frontend directory, let’s install the Tailwind library:

npm install -D tailwindcss
npx tailwindcss init

Add the highlighted line below to the new tailwind.config.js file, created in the last step:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Finally, create a new index.css file in the frontend/src directory and add the following Tailwind directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

Create another file (index.js) in the frontend/src directory and add the following code. This is straightforward, common code for launching a new react App component in a virtual DOM:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Now, let’s start work on the App.js component, which will contain the core code of our new quiz app. Create a third file in the frontend/src directory and name it App.js (following the proper naming convention of capitalizing the first letter of the new component file). Place the following code at the top:

import React, { useState, useEffect } from "react";

function App() {
  const [questions, setQuestions] = useState(null);
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [showScore, setShowScore] = useState(false);
  const [score, setScore] = useState(0);

  useEffect(() => {
    setQuestions([
      {
        questionText: "Where is the Taj Mahal?",
        answerOptions: [
          { answerText: "Agra", isCorrect: true },
          { answerText: "New Delhi", isCorrect: false },
          { answerText: "Mumbai", isCorrect: false },
          { answerText: "Chennai", isCorrect: false },
        ],
      },
      {
        questionText: "Where is the Great Wall of China?",
        answerOptions: [
          { answerText: "China", isCorrect: true },
          { answerText: "Beijing", isCorrect: false },
          { answerText: "Shanghai", isCorrect: false },
          { answerText: "Tianjin", isCorrect: false },
        ],
      },
    ]);
  }, []);

In the above code:

  • Lines 1-7: Initializing the App and adding our pieces of state:
    • Questions (eventually loaded from our source API) will be managed by questions and currentQuestion.
    • showScore and score will be used for managing the points accumulated from correct answers and displaying the result at the end of the quiz.
  • Lines 9-30: In this initial testing phase, we’ll load the quiz questions and answers through the use of a useEffect() hook. In a later phase, this will contain a method for fetching the data through the backend API.

The next bit looks like this:

useEffect(() => {
    if (!showScore) return;
    const launchId = window.launchId;
    fetch("/api/score/" + launchId + "/" + score + "/", {
      method: "POST",
    })
      .then((response) => response.json())
      .catch((error) => console.error(error));
  }, [showScore, score]);
  • Line 32: The declaration of another useEffect() hook. The intention of this one will be to POST the final grade to the platform once the state of showScore is true. This will happen after the final question has been answered.
  • Line 33: Since our useEffect may fire multiple times based on the last line and the array of dependencies containing ‘[showScore, score]’, we don’t want to do anything until showScore’s state is true.
  • Line 34: The unique launch ID (more on this later) that’s contained in the Flask template (window) is retrieved for inclusion in the POST to the platform.
  • Lines 35-39: The Fetch API is used here to post the final score through our flask implementation, taking a JSON response and any error output. This could be implemented in a number of ways, including using the Axios library for making promise-based requests. This final code assumes that we are using ‘npm run build’ instead of running in dev mode, so the path to /api/score doesn’t include the full path to our local flask dev server. In other words, this assumes that we are including the static product (.js/.css files) in the backend. More clarification on this later.

Our quiz app will invoke a separate button for each answer. Here’s an event handler that fires once a button is clicked:

const handleAnswerButtonClick = (isCorrect) => {
    if (isCorrect === true) {
      setScore((score) => score + 1);
    }
    const nextQuestion = currentQuestion + 1;
    if (nextQuestion < questions.length) {
      setCurrentQuestion(nextQuestion);
    } else {
      setShowScore(true);
    }
  };

Lines 42-45: The handler receives the current correct answer property and adds to the score for a correct answer.
Lines 46-50: Set the next question and check if we have are finished with all the questions. If so, we will set the state for rendering the final score.

Our simple quiz app uses a conditional (ternary) operators to render the questions and final score. We’re using Tailwind CSS for styling.

return (
    <>
      <div className="bg-white pt-10 pb-8 shadow-xl sm:mx-auto sm:rounded-lg sm:px-10 w-128">
        <h1 className="mb-4 text-4xl font-extrabold">Quiz</h1>
        {showScore ? (
          <div>
            You scored {score} out of {questions.length}
          </div>
        ) : (
          <>
            <p>
              Question {currentQuestion + 1} of {questions && questions.length}
            </p>
            <p>----</p>
            <p className="mb-4">
              {questions && questions[currentQuestion].questionText}
            </p>

            <div className="flex space-x-4">
              {questions &&
                questions[currentQuestion].answerOptions.map(
                  (answerOptions, index) => (
                    <button
                      key={index}
                      className="text-white sm:px-2 sm:py-2 bg-sky-700 hover:bg-sky-800 rounded"
                      onClick={() =>
                        handleAnswerButtonClick(answerOptions.isCorrect)
                      }
                    >
                      {answerOptions.answerText}
                    </button>
                  )
                )}
            </div>
          </>
        )}
      </div>
    </>
  );
}

export default App;

Once all the code comes together, run the app to ensure that all is working. The score submission will throw an error until the backend pieces are completed in the next section.

npm run

Connect to http://127.0.0.1:3000 to give the quiz frontend a test

Stay tuned for several new parts in this series, where we’ll complete the following:

  • Implement the backend APIs and routes
  • Set up a database (SQLite) for storing and retrieving quiz questions through a set of database methods for running queries.
  • Complete the Flask template for final rendering of our react component.
  • Test using the saltire platform emulator.
  • Create a diagram of all of the moving parts (LTI (Platform <-> Tool), Flask (LTI, API <-> Database), frontend.

Leave a Reply

Your email address will not be published. Required fields are marked *

+ 34 = 35

This site uses Akismet to reduce spam. Learn how your comment data is processed.