React Redux Quiz Application Code

App.js

import React from 'react';
import './App.css';
import { connect } from 'react-redux';
// import logo from './logo.svg'; // Logo import seems unused in the provided snippet
// import ReduxProvider from './redux/ReduxProvider'; // ReduxProvider is likely used higher up the component tree
import Game from "./Game";
import { questionAnswer, changeQuestion, submit, initQuestions } from './redux/actions';

function App(props) {
  return (
    <div>
      <Game
        onQuestionAnswer={(answer) => {
          props.dispatch(questionAnswer(props.currentQuestion, answer));
        }}
        presentQuestion={props.currentQuestion}
        questionsAvailable={props.questions}
        totalScore={props.score}
        gameStatus={props.finished}
        onChangeQuestion={(index) => {
          props.dispatch(changeQuestion(index));
        }}
        onSubmit={(questions) => {
          props.dispatch(submit(questions));
        }}
        onInitQuestions={(questions) => {
          props.dispatch(initQuestions(questions));
        }}
        // Assuming 'question' prop is needed by Game, derived from state
        question={props.questions[props.currentQuestion]}
      />
    </div>
  );
}

function mapStateToProps(state) {
  return {
    ...state
  };
}

export default connect(mapStateToProps)(App);

App.css

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #09d3ac;
}

ReduxProvider.js

import { Provider } from 'react-redux';
import GlobalState from './reducers';
import { createStore } from 'redux';
import React from 'react';
import App from '../App';
// Assuming 'assets' was intended instead of 'asserts'
// import { questions } from "../assets/mock-data"; // Using fetched data instead of mock

export default class ReduxProvider extends React.Component {
  constructor(props) {
    super(props);
    this.initialState = {
      score: 0,
      finished: false,
      currentQuestion: 0,
      questions: [] // Initial questions will be fetched
    };
    this.store = this.configureStore();
  }

  configureStore() {
    return createStore(GlobalState, this.initialState);
  }

  render() {
    return (
      <Provider store={this.store}>
        <App />
      </Provider>
    );
  }
}

reducers.js

import { combineReducers } from 'redux';
import { QUESTION_ANSWER, CHANGE_QUESTION, INIT_QUESTIONS, SUBMIT } from './actions';

function score(state = 0, action = {}) {
  switch (action.type) {
    case SUBMIT:
      let correctQuestions = 0;
      // Ensure action.questions is defined and is an array
      if (Array.isArray(action.questions)) {
          for (let i = 0; i < action.questions.length; i++) {
            if (action.questions[i].answer === action.questions[i].userAnswer) {
              correctQuestions++;
            }
          }
      }
      return correctQuestions;
    default:
      return state;
  }
}

function finished(state = false, action = {}) {
  switch (action.type) {
    case SUBMIT:
      return true; // Corrected: return boolean true directly
    case INIT_QUESTIONS: // Reset finished state when new questions are loaded
        return false;
    default:
      return state;
  }
}

function currentQuestion(state = 0, action = {}) {
  switch (action.type) {
    case CHANGE_QUESTION:
      // Add boundary checks if necessary
      return action.index;
    case INIT_QUESTIONS: // Reset to first question on init
        return 0;
    case SUBMIT: // Optionally reset or stay on last question
        return state;
    default:
      return state;
  }
}

function questions(state = [], action = {}) {
  switch (action.type) {
    case QUESTION_ANSWER:
      return state.map((question, i) => {
        // Ensure payload exists and index is valid
        if (action.payload && action.payload.index === i) {
            return { ...question, userAnswer: action.payload.answer };
        } else {
            return question;
        }
      });
    case INIT_QUESTIONS:
      // Ensure action.questions is an array, add default userAnswer
      return Array.isArray(action.questions) ? action.questions.map(q => ({...q, userAnswer: undefined })) : [];
    default:
      return state;
  }
}

const GlobalState = combineReducers({
  score,
  finished,
  currentQuestion,
  questions
});

export default GlobalState;

actions.js

export const QUESTION_ANSWER = 'QUESTION_ANSWER';
export const CHANGE_QUESTION = 'CHANGE_QUESTION';
export const SUBMIT = 'SUBMIT';
export const INIT_QUESTIONS = 'INIT_QUESTIONS';

export function questionAnswer(index, answer) {
  return { type: QUESTION_ANSWER, payload: { index, answer } };
}

export function changeQuestion(index) {
  return { type: CHANGE_QUESTION, index };
}

export function submit(questions) {
  return { type: SUBMIT, questions };
}

export function initQuestions(questions) {
  return { type: INIT_QUESTIONS, questions };
}

Game.js

import React from 'react';
import './Game.css';

const API_URL = "https://quiz.dit.upm.es/api/quizzes/random10wa?token=291da9667a66a796f79a";

export default class Game extends React.Component {

  componentDidMount() {
    this.fetchQuestions();
  }

  fetchQuestions = () => {
    fetch(API_URL)
      .then(res => {
          if (!res.ok) {
              throw new Error(`HTTP error! status: ${res.status}`);
          }
          return res.json();
      })
      .then(
        (result) => {
          // console.log(result, "request");
          // Initialize questions in Redux store
          this.props.onInitQuestions(result);
        },
        (error) => {
          console.error("Error fetching questions:", error);
          // Handle error state, maybe dispatch an action
          this.props.onInitQuestions([]); // Pass empty array on error
        }
      );
  }

  resetGame = () => {
      // Re-fetch questions to reset the game
      this.fetchQuestions();
      // Note: Reducer for INIT_QUESTIONS should reset score, finished status, currentQuestion index etc.
  }

  renderTips() {
    // Ensure question and tips exist
    if (!this.props.question || !Array.isArray(this.props.question.tips) || this.props.question.tips.length === 0) {
        return <p>No tips available for this question.</p>;
    }
    return (
      <ul>
        {this.props.question.tips.map((tip, index) => (
          <li key={index}>{tip}</li>
        ))}
      </ul>
    );
  }

  getTipsSection() {
    return (
      <div className="tips-section">
        <h4>Tips:</h4>
        {this.renderTips()}
      </div>
    );
  }

  renderEndGame() {
    return (
      <div className="end-game-screen">
        <h3>Quiz Game Finished!</h3>
        <p>Your score is: {this.props.totalScore} out of {this.props.questionsAvailable.length}.</p>
        <button onClick={this.resetGame}>Reset Game</button>
      </div>
    );
  }

  renderNoQuestions() {
    return (
      <div className="no-questions-screen">
        <h3>Quiz Game Error</h3>
        <p>There was a problem loading questions.</p>
        <button onClick={this.resetGame}>Retry</button>
      </div>
    );
  }

  // Check if the previous button should be disabled
  isPreviousButtonDisabled() {
    return this.props.presentQuestion === 0;
  }

  // Check if the next button should be disabled
  isNextButtonDisabled() {
    return this.props.presentQuestion >= this.props.questionsAvailable.length - 1;
  }

  handleAnswerChange = (e) => {
      this.props.onQuestionAnswer(e.target.value);
  }

  render() {
    const { gameStatus, questionsAvailable, presentQuestion, question, onSubmit } = this.props;

    if (gameStatus === true) {
      return this.renderEndGame();
    }

    if (!Array.isArray(questionsAvailable) || questionsAvailable.length === 0) {
      return this.renderNoQuestions();
    }

    // Ensure 'question' prop is valid before rendering
    if (!question) {
        // This might happen briefly while loading or if index is out of bounds
        return <div>Loading question...</div>;
    }

    return (
      <div className="game-container">
        <div className="game-header">
          <h3>Quiz Game</h3>
        </div>

        <div className="question-area">
          {question.attachment && question.attachment.url &&
            <img src={question.attachment.url} alt={`Question ${presentQuestion + 1} visual aid`} className="question-image"/>
          }
          <div className="question-text">
            <h4>Question {presentQuestion + 1} of {questionsAvailable.length}</h4>
            <p>{question.question}</p>
          </div>
          <div className="answer-input">
            <label htmlFor="userAnswer">Your Answer:</label>
            <input
                type="text"
                id="userAnswer"
                value={question.userAnswer || ''} // Controlled component
                onChange={this.handleAnswerChange}
            />
          </div>
          {this.getTipsSection()}
        </div>

        <div className="navigation-buttons">
          <button
            onClick={() => this.props.onChangeQuestion(presentQuestion - 1)}
            disabled={this.isPreviousButtonDisabled()}
          >
            Previous Question
          </button>
          <button
            onClick={() => this.props.onChangeQuestion(presentQuestion + 1)}
            disabled={this.isNextButtonDisabled()}
          >
            Next Question
          </button>
          <button onClick={() => onSubmit(questionsAvailable)}>
            Submit Quiz
          </button>
          <button onClick={this.resetGame}>
            Reset Game
          </button>
        </div>
      </div>
    );
  }
}