diff --git a/index.html b/index.html index 534cd886..03642f99 100644 --- a/index.html +++ b/index.html @@ -38,8 +38,7 @@

JavaScript Quiz

- - + @@ -51,4 +50,4 @@

JavaScript Quiz

- \ No newline at end of file + diff --git a/requirements.md b/requirements.md new file mode 100644 index 00000000..077eada6 --- /dev/null +++ b/requirements.md @@ -0,0 +1,102 @@ +# JavaScript Quiz Project Requirements + +## Overview +This application is a small quiz game with a countdown timer. The quiz should display multiple-choice questions one at a time, track the correct answers, and show an end screen after the quiz completes or the timer reaches zero. + +## App Structure +- Single-page quiz experience with two main views: + - `quizView`: visible during the quiz questions + - `endView`: visible after the quiz ends +- A timer display should appear in the top-right corner of the quiz view. +- The quiz should be implemented with JavaScript and should not require page reloads to move between questions or show results. + +## UI Layout and Elements +### Quiz View +- Progress bar at the top that updates to reflect current question progress. +- Question count text such as `Question 1 of 5`. +- Question text displayed clearly. +- A list of radio button choices for the current question. +- A visible timer section showing the remaining time in `MM:SS` format. +- A button labeled `Answer` to submit the current answer and move to the next question. + +### End View +- Hidden by default until the quiz ends. +- Displays final score text like `You scored X out of Y correct answers!`. +- Displays a horizontal progress bar representing score percentage. +- A `Restart Quiz` button that resets the quiz and timer. + +## Functional Behavior +### Quiz Data and Initialization +- The quiz should contain an array of `Question` objects. +- Each question includes: + - question text + - an array of choices + - the correct answer value + - a difficulty level (optional) +- Quiz state includes: + - `questions` + - `currentQuestionIndex` + - `correctAnswers` + - `timeLimit` + - `timeRemaining` +- When the app loads: + - shuffle the question order + - show the first question + - initialize the timer display to the quiz duration + - start the countdown + +### Question Display and Answering +- Each question must display current text and choices. +- Choices should be radio inputs sharing the same `name` so only one can be selected. +- When `Answer` is clicked: + - read the selected radio choice + - if an answer is selected, validate it against the current question's answer + - increment `correctAnswers` only for correct answers + - move to the next question + - display the next question or show results if the quiz has ended +- If no answer is selected, the app may do nothing or simply not advance. + +### Progress Tracking +- The progress bar width should reflect the portion of answered questions, e.g. `((currentQuestionIndex + 1) / totalQuestions) * 100%`. +- The question count text should always use the current question number and total questions. + +## Timer Requirements +- The timer starts when the quiz begins. +- It counts down from `timeRemaining` once per second using `setInterval()`. +- Timer text must update every second in `MM:SS` format. +- If the timer reaches zero: + - stop the timer interval + - set `timeRemaining` to 0 + - automatically end the quiz and show the result view +- If the quiz ends before time runs out: + - clear the timer interval immediately to prevent background execution + +## End-of-Quiz Behavior +- The quiz ends when: + - the last question has been answered, or + - the timer reaches zero. +- When the quiz ends: + - hide the quiz view + - show the end view + - clear the timer interval + - show the user score text + - update the result progress bar based on score percentage + +## Restart Behavior +- When `Restart Quiz` is clicked: + - hide the end view + - show the quiz view + - reset `currentQuestionIndex` to 0 + - reset `correctAnswers` to 0 + - reset `timeRemaining` to `timeLimit` + - shuffle questions again + - show the first question + - update the timer display to the full duration + - start the countdown timer again + +## Additional Notes +- Keep all application logic inside `src/index.js`. +- `src/quiz.js` should define the `Quiz` class and state methods like `getQuestion()`, `moveToNextQuestion()`, `checkAnswer()`, and `hasEnded()`. +- `src/question.js` should define the `Question` class, including any helper methods such as `shuffleChoices()` if needed. +- Ensure the timer display is accurate and responsive when restarting the quiz. +- Avoid unnecessary timers or memory leaks by always clearing intervals when they are no longer needed. diff --git a/src/index.js b/src/index.js index 03737ba3..b6a06d9c 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,8 @@ document.addEventListener("DOMContentLoaded", () => { // End view elements const resultContainer = document.querySelector("#result"); + const resultProgressBar = document.querySelector("#resultProgressBar"); + const restartButton = document.querySelector("#restartButton"); /************ SET VISIBILITY OF VIEWS ************/ @@ -29,8 +31,9 @@ document.addEventListener("DOMContentLoaded", () => { new Question("What is 2 + 2?", ["3", "4", "5", "6"], "4", 1), new Question("What is the capital of France?", ["Miami", "Paris", "Oslo", "Rome"], "Paris", 1), new Question("Who created JavaScript?", ["Plato", "Brendan Eich", "Lea Verou", "Bill Gates"], "Brendan Eich", 2), - new Question("What is the mass–energy equivalence equation?", ["E = mc^2", "E = m*c^2", "E = m*c^3", "E = m*c"], "E = mc^2", 3), + new Question("What is the mass–energy equivalence equation?", ["E = mc^2", "E = m*c^2", "E = m*c^3", "E = m*c"], "E = m*c^2", 3), // Add more questions here + new Question("Where is France", ["in Europe", "In Asia", "in Africa", "in South America"], "in Europe", 2), ]; const quizDuration = 120; // 120 seconds (2 minutes) @@ -45,26 +48,58 @@ document.addEventListener("DOMContentLoaded", () => { /************ SHOW INITIAL CONTENT ************/ - // Convert the time remaining in seconds to minutes and seconds, and pad the numbers with zeros if needed - const minutes = Math.floor(quiz.timeRemaining / 60).toString().padStart(2, "0"); - const seconds = (quiz.timeRemaining % 60).toString().padStart(2, "0"); - - // Display the time remaining in the time remaining container + // Get the timer display container and its span element const timeRemainingContainer = document.getElementById("timeRemaining"); - timeRemainingContainer.innerText = `${minutes}:${seconds}`; + const timeRemainingSpan = timeRemainingContainer.querySelector("span"); - // Show first question - showQuestion(); + let timer; + + function formatTime(seconds) { + const minutes = Math.floor(seconds / 60).toString().padStart(2, "0"); + const remainingSeconds = (seconds % 60).toString().padStart(2, "0"); + return `${minutes}:${remainingSeconds}`; + } + function updateTimerDisplay() { + timeRemainingSpan.innerText = formatTime(quiz.timeRemaining); + } - /************ TIMER ************/ + function startTimer() { + if (timer) { + clearInterval(timer); + } + + updateTimerDisplay(); + + timer = setInterval(() => { + quiz.timeRemaining -= 1; + + if (quiz.timeRemaining <= 0) { + quiz.timeRemaining = 0; + updateTimerDisplay(); + clearInterval(timer); + + timer = null; + showResults(); + return; + } + + updateTimerDisplay(); + }, 1000); + } + + updateTimerDisplay(); + + // Show first question + showQuestion(); + startTimer(); - let timer; /************ EVENT LISTENERS ************/ nextButton.addEventListener("click", nextButtonHandler); + restartButton.addEventListener("click", restartButtonHandler); @@ -98,21 +133,23 @@ document.addEventListener("DOMContentLoaded", () => { // // 1. Show the question // Update the inner text of the question container element and show the question text - - + questionContainer.innerText = question.text; + // 2. Update the green progress bar // Update the green progress bar (div#progressBar) width so that it shows the percentage of questions answered - progressBar.style.width = `65%`; // This value is hardcoded as a placeholder - + // const progress = (quiz.currentQuestionIndex + 1) * 10; + const progress = ((quiz.currentQuestionIndex + 1) / quiz.questions.length) * 100; + // console.log(`progress = ${Math.round(progress)}%`) + // progressBar.style.width = `25%`; // This value is hardcoded as a placeholder + progressBar.style.width = `${Math.round(progress)}%` // 3. Update the question count text // Update the question count (div#questionCount) show the current question out of total questions - questionCount.innerText = `Question 1 of 10`; // This value is hardcoded as a placeholder - - + // questionCount.innerText = `Question ${quiz.currentQuestionIndex + 1} of 10`; // This value is hardcoded as a placeholder + questionCount.innerText = `Question ${quiz.currentQuestionIndex + 1} of ${quiz.questions.length}`; // 4. Create and display new radio input element with a label for each choice. // Loop through the current question `choices`. @@ -128,6 +165,21 @@ document.addEventListener("DOMContentLoaded", () => { // Hint 3: You can use the `element.appendChild()` method to append an element to the choices container. // Hint 4: You can use the `element.innerText` property to set the inner text of an element. + question.choices.forEach((choice) => { + const choiceInput = document.createElement("input"); + choiceInput.type = "radio"; + choiceInput.name = "choice"; + choiceInput.value = choice; + + const choiceLabel = document.createElement("label"); + choiceLabel.innerText = choice; + + const lineBreak = document.createElement("br"); + + choiceContainer.appendChild(choiceInput); + choiceContainer.appendChild(choiceLabel); + choiceContainer.appendChild(lineBreak); + }); } @@ -140,27 +192,41 @@ document.addEventListener("DOMContentLoaded", () => { // YOUR CODE HERE: // // 1. Get all the choice elements. You can use the `document.querySelectorAll()` method. + console.log("Get all the choice elements = ") + const choiceElements = document.querySelectorAll('input[name="choice"]'); + console.log(choiceElements); // 2. Loop through all the choice elements and check which one is selected // Hint: Radio input elements have a property `.checked` (e.g., `element.checked`). // When a radio input gets selected the `.checked` property will be set to true. // You can use check which choice was selected by checking if the `.checked` property is true. - + choiceElements.forEach((radio) => { + if (radio.checked) { + selectedAnswer = radio.value; + } + }); // 3. If an answer is selected (`selectedAnswer`), check if it is correct and move to the next question // Check if selected answer is correct by calling the quiz method `checkAnswer()` with the selected answer. // Move to the next question by calling the quiz method `moveToNextQuestion()`. // Show the next question by calling the function `showQuestion()`. + if (selectedAnswer) { + quiz.checkAnswer(selectedAnswer); + quiz.moveToNextQuestion(); + showQuestion(); + } } function showResults() { + if (timer) { + clearInterval(timer); + timer = null; + } - // YOUR CODE HERE: - // // 1. Hide the quiz view (div#quizView) quizView.style.display = "none"; @@ -168,7 +234,28 @@ document.addEventListener("DOMContentLoaded", () => { endView.style.display = "flex"; // 3. Update the result container (div#result) inner text to show the number of correct answers out of total questions - resultContainer.innerText = `You scored 1 out of 1 correct answers!`; // This value is hardcoded as a placeholder + resultContainer.innerText = `You scored ${quiz.correctAnswers} out of ${quiz.questions.length} correct answers!`; + + const resultPercentage = (quiz.correctAnswers / quiz.questions.length) * 100; + resultProgressBar.style.width = `${resultPercentage}%`; + } + + + + function restartButtonHandler() { + // Hide the end view and show the quiz view again + endView.style.display = "none"; + quizView.style.display = "block"; + + // Reset the quiz state + quiz.currentQuestionIndex = 0; + quiz.correctAnswers = 0; + quiz.timeRemaining = quiz.timeLimit; + + // Shuffle the questions and show the first question + quiz.shuffleQuestions(); + showQuestion(); + startTimer(); } -}); \ No newline at end of file +}); diff --git a/src/question.js b/src/question.js index 68f6631a..74405c07 100644 --- a/src/question.js +++ b/src/question.js @@ -2,6 +2,30 @@ class Question { // YOUR CODE HERE: // // 1. constructor (text, choices, answer, difficulty) + constructor(text, choices, answer, difficulty) { + this.text = text + this.choices = choices + this.answer = answer + // difficulty: with 1 being the easiest and 3 being the hardest. + this.difficulty = difficulty + //console.log("choices = ", choices) + } // 2. shuffleChoices() + shuffleChoices() { + console.log("choices = ", this.choices) + + for(let i=0; i < this.choices.length; i++) { + // console.log("Choise = ", this.choices[i]) + const j = Math.floor(Math.random() * this.choices.length); + + // swap elements + const val = this.choices[i] + this.choices[i] = this.choices[j] + this.choices[j] = val + } + + // console.log("Choise = ", this.choices) + } + } \ No newline at end of file diff --git a/src/quiz.js b/src/quiz.js index d94cfd14..c40b9f8f 100644 --- a/src/quiz.js +++ b/src/quiz.js @@ -2,14 +2,78 @@ class Quiz { // YOUR CODE HERE: // // 1. constructor (questions, timeLimit, timeRemaining) + constructor (questions, timeLimit, timeRemaining) { + this.questions = questions + this.timeLimit = timeLimit + this.timeRemaining = timeRemaining + this.correctAnswers = 0 + this.currentQuestionIndex = 0 + } // 2. getQuestion() + getQuestion() { + return this.questions[this.currentQuestionIndex] + } // 3. moveToNextQuestion() + moveToNextQuestion() { + this.currentQuestionIndex++ + } // 4. shuffleQuestions() + shuffleQuestions() { + + for(let i=0; i < this.questions.length; i++) { + + const j = Math.floor(Math.random() * this.questions.length); + + // swap elements + const val = this.questions[i] + this.questions[i] = this.questions[j] + this.questions[j] = val + } + } // 5. checkAnswer(answer) + checkAnswer(answer) { + if (answer === this.questions[this.currentQuestionIndex].answer) { + this.correctAnswers++ + } + } // 6. hasEnded() -} \ No newline at end of file + hasEnded() { + if (this.currentQuestionIndex < this.questions.length) { + return false + } else { + return true + } + } + + filterQuestionsByDifficulty(difficulty) { + if (typeof difficulty === 'number' && difficulty >= 1 && difficulty <= 3) { + this.questions = this.questions.filter((question) => { + return question.difficulty === difficulty; + }); + } + // If invalid, do nothing + // console.log(this.questions) + } + + averageDifficulty() { + if (this.questions.length === 0) return 0; + + const sum = this.questions.reduce( + (acc, question) => acc + question.difficulty, 0); + return sum / this.questions.length; + } + + showQuestion() { + // this.currentQuestionIndex + const contentObj = document.getElementByClassName("content") + console.log("contentObj = ", contentObj) + } + // const mainTitle = document.getElementById("main-title") +} + +