From ea5a4a6941227d29a55839cf2a3fa01e6dbbb340 Mon Sep 17 00:00:00 2001 From: Alaa Date: Thu, 25 Sep 2025 20:42:41 +0200 Subject: [PATCH 1/4] Add the hint feature --- src/app.js | 9 +++++---- src/constants.js | 2 ++ src/pages/questionPage.js | 33 +++++++++++++++++++++++++++++++++ src/views/questionView.js | 8 ++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index e197404..7111582 100644 --- a/src/app.js +++ b/src/app.js @@ -16,10 +16,6 @@ const loadApp = () => { * - currentQuestionIndex back to 0 * - clears all selected answers */ -export const resetQuizState = () => { - quizData.currentQuestionIndex = 0; - quizData.questions.forEach((q) => (q.selected = null)); -}; /** * Move to next question @@ -34,6 +30,11 @@ export const goToNextQuestion = () => { return false; }; +export const resetQuizState = () => { + quizData.currentQuestionIndex = 0; + quizData.questions.forEach((q) => (q.selected = null)); +}; + /** * Calculate current score */ diff --git a/src/constants.js b/src/constants.js index 68a8f9d..a7d1be6 100644 --- a/src/constants.js +++ b/src/constants.js @@ -10,3 +10,5 @@ export const START_QUIZ_BUTTON_ID = 'start-quiz-button'; export const ANSWERS_LIST_ID = 'answers-list'; export const NEXT_QUESTION_BUTTON_ID = 'next-question-button'; export const AVOID_QUESTION_BUTTON_ID = 'avoid-question-button'; +export const ELEMINATE_TWO_ANSWERS_BUTTON_ID = 'eliminate-two-answers-button'; +export const RESTART_QUIZ = 'restart-quiz'; diff --git a/src/pages/questionPage.js b/src/pages/questionPage.js index 35b8f71..3583608 100644 --- a/src/pages/questionPage.js +++ b/src/pages/questionPage.js @@ -3,10 +3,13 @@ import { NEXT_QUESTION_BUTTON_ID, USER_INTERFACE_ID, AVOID_QUESTION_BUTTON_ID, + ELEMINATE_TWO_ANSWERS_BUTTON_ID, + RESTART_QUIZ, } from '../constants.js'; import { createQuestionElement } from '../views/questionView.js'; import { createAnswerElement } from '../views/answerView.js'; import { quizData } from '../data.js'; +import { resetQuizState } from '../app.js'; // Step 1: Store selected answer const storeAnswer = (questionIndex, selectedOption) => { @@ -21,6 +24,7 @@ export const initQuestionPage = () => { const currentQuestion = quizData.questions[quizData.currentQuestionIndex]; const questionElement = createQuestionElement(currentQuestion.text); + userInterface.appendChild(questionElement); const answersListElement = document.getElementById(ANSWERS_LIST_ID); @@ -69,11 +73,22 @@ export const initQuestionPage = () => { // disabled document.getElementById(AVOID_QUESTION_BUTTON_ID).disabled = true; + document.getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID).disabled = true; }); answersListElement.appendChild(answerElement); } + document + .getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID) + .addEventListener('click', () => { + const allListItems = Array.from( + answersListElement.querySelectorAll('li') + ); + hint(currentQuestion, allListItems); + document.getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID).disabled = true; + }); + document .getElementById(NEXT_QUESTION_BUTTON_ID) .addEventListener('click', nextQuestion); @@ -81,6 +96,10 @@ export const initQuestionPage = () => { document .getElementById(AVOID_QUESTION_BUTTON_ID) .addEventListener('click', avoidQuestion); + + document + .getElementById(RESTART_QUIZ) + .addEventListener('click', resetQuizState); }; const nextQuestion = () => { @@ -95,3 +114,17 @@ const avoidQuestion = () => { initQuestionPage(); // display the new question }; + +const hint = (currentQuestion, allListItems) => { + const wrongItems = allListItems.filter((li) => { + return li.dataset.key !== currentQuestion.correct; + }); //get all the wrong options + const elements = new Set(); + + while (elements.size < 2) { + const randomIndex = Math.floor(Math.random() * wrongItems.length); + elements.add(wrongItems[randomIndex]); + } + + elements.forEach((ele) => (ele.hidden = true)); +}; diff --git a/src/views/questionView.js b/src/views/questionView.js index c949bc6..1fa5409 100644 --- a/src/views/questionView.js +++ b/src/views/questionView.js @@ -2,6 +2,8 @@ import { ANSWERS_LIST_ID } from '../constants.js'; import { NEXT_QUESTION_BUTTON_ID, AVOID_QUESTION_BUTTON_ID, + ELEMINATE_TWO_ANSWERS_BUTTON_ID, + RESTART_QUIZ, } from '../constants.js'; /** @@ -13,8 +15,10 @@ export const createQuestionElement = (question) => { // I use String.raw just to get fancy colors for the HTML in VS Code. element.innerHTML = String.raw` +

${question}

+ @@ -26,6 +30,10 @@ export const createQuestionElement = (question) => { Avoid question + + `; return element; From db60107f535dfc7f1326509ec5deb20b66bba2ff Mon Sep 17 00:00:00 2001 From: Alaa Date: Thu, 25 Sep 2025 20:46:32 +0200 Subject: [PATCH 2/4] Add reset button --- src/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app.js b/src/app.js index 7111582..0899f78 100644 --- a/src/app.js +++ b/src/app.js @@ -33,6 +33,9 @@ export const goToNextQuestion = () => { export const resetQuizState = () => { quizData.currentQuestionIndex = 0; quizData.questions.forEach((q) => (q.selected = null)); + quizData.score = 0; + console.log('score: ', quizData.score); + initWelcomePage(); }; /** From 07d22641292758ff92742c351036683653bc2e52 Mon Sep 17 00:00:00 2001 From: Majd Hamde Date: Sat, 27 Sep 2025 02:51:55 +0200 Subject: [PATCH 3/4] major changes with styling and logic --- public/style.css | 865 +++++++++++++++++++++++++++++++++++++- server.js | 35 ++ src/app.js | 213 +++++++++- src/constants.js | 56 ++- src/data.js | 3 +- src/pages/endPage.js | 109 ++++- src/pages/questionPage.js | 642 ++++++++++++++++++++++++---- src/pages/welcomePage.js | 98 ++++- src/views/questionView.js | 74 ++-- src/views/welcomeView.js | 73 ++-- 10 files changed, 1974 insertions(+), 194 deletions(-) create mode 100644 server.js diff --git a/public/style.css b/public/style.css index 9f7ef7f..4a5a90c 100644 --- a/public/style.css +++ b/public/style.css @@ -10,7 +10,13 @@ --green-500: #66bb6a; /* primary accent */ --green-600: #43a047; /* primary button */ --green-700: #2e7d32; /* deep salad */ - --accent-yellow: #ffeb3b; + /* Accent palette (salad-inspired, not only green) */ + --accent-tomato: #ff6b6b; /* tomato red */ + --accent-carrot: #ffa62b; /* carrot orange */ + --accent-lemon: #ffd93d; /* lemon yellow */ + --accent-fresh-green: #6bcb77; /* fresh green highlight */ + --accent-info: #2196f3; /* professional blue for hints */ + --accent-yellow: var(--accent-lemon); --text-strong: #eaf7ea; --text-muted: #cfe8d0; --glass-bg: rgba(255, 255, 255, 0.08); @@ -71,7 +77,7 @@ body::after { .centered { width: min(92vw, 720px); max-height: min(86vh, 900px); - overflow: hidden; + overflow-y: auto; /* Scroll if content is too long on small screens */ backdrop-filter: blur(14px) saturate(120%); -webkit-backdrop-filter: blur(14px) saturate(120%); background: linear-gradient( @@ -184,11 +190,16 @@ ul li { ul li:hover { background: linear-gradient( 135deg, - #ffeb3b, - #ffee58 + var(--answer-hover-accent, var(--accent-lemon)), + var(--answer-hover-accent, var(--accent-lemon)) ); /* brighter lemon on hover */ transform: translateY(-2px) scale(1.015) rotate(-0.25deg); - box-shadow: 0 10px 24px rgba(255, 235, 59, 0.35); + box-shadow: 0 10px 24px + color-mix( + in lab, + var(--answer-hover-accent, #ffd93d) 35%, + rgba(0, 0, 0, 0.2) + ); filter: brightness(1.04); } @@ -262,8 +273,9 @@ button { text-transform: capitalize; box-shadow: 0 6px 20px rgba(46, 125, 50, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.25); - transition: transform 160ms ease, box-shadow 220ms ease, - background-position 260ms ease, filter 200ms ease; + /* Hover interactions ~200ms */ + transition: transform 200ms ease, box-shadow 200ms ease, + background-position 200ms ease, filter 200ms ease; } button::before { @@ -272,14 +284,78 @@ button::before { transform: translateY(1px) rotate(-10deg); } +/* Function-specific button classes */ +.btn-primary { + --btn-bg: linear-gradient(135deg, var(--accent-fresh-green), #3fbf6b); + color: #0b1e0b; + padding: 14px 20px; /* Slightly larger for primary */ + box-shadow: 0 8px 24px rgba(59, 191, 107, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.3); +} + +.btn-warning { + --btn-bg: linear-gradient(135deg, var(--accent-carrot), #ff8c00); + color: #2b1200; + border: 1px dashed rgba(255, 165, 0, 0.6); /* Dashed for caution */ +} + +.btn-info { + --btn-bg: linear-gradient(135deg, var(--accent-info), #1976d2); + color: #ffffff; + box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.25); +} + +.btn-info:hover { + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.02); + } +} + button:hover { background-position: 100% 0%; transform: translateY(-2px) rotate(-0.35deg) scale(1.02); box-shadow: 0 14px 32px rgba(46, 125, 50, 0.55), - inset 0 1px 0 rgba(255, 255, 255, 0.3); + inset 0 1px 0 rgba(255, 255, 255, 0.3), + 0 0 0 6px + color-mix( + in lab, + var(--answer-hover-accent, rgba(102, 187, 106, 0.18)) 30%, + transparent + ); filter: saturate(1.05); } +/* Icon overrides for specific buttons */ +#next-question-button::before, +.btn-primary::before { + content: '➡️'; + font-size: 16px; + transform: translateY(0px); +} + +#avoid-question-button::before, +.btn-warning::before { + content: '⚠️'; + font-size: 16px; + transform: translateY(0px); +} + +#eliminate-two-answers-button::before, +.btn-info::before { + content: '💡'; + font-size: 16px; + transform: translateY(0px); +} + button:active { transform: translateY(0) scale(0.98); } @@ -289,17 +365,40 @@ button:active { --btn-bg: linear-gradient(135deg, var(--green-600), var(--green-500)); } #next-question-button { - --btn-bg: linear-gradient(135deg, #66bb6a, #43a047); + /* Fresh green */ + --btn-bg: linear-gradient(135deg, var(--accent-fresh-green), #3fbf6b); + color: #0b1e0b; } #avoid-question-button { - --btn-bg: linear-gradient(135deg, #a5d6a7, #66bb6a); - color: #0a1c0a; + /* Carrot */ + --btn-bg: linear-gradient(135deg, var(--accent-carrot), #ff8c00); + color: #2b1200; +} +#eliminate-two-answers-button { + /* Info blue */ + --btn-bg: linear-gradient(135deg, var(--accent-info), #1976d2); + color: #ffffff; } -#restart-quiz-button { +#reset-quiz-button { --btn-bg: linear-gradient(135deg, #ffeb3b, #a5d6a7); color: #163516; } +/* Rules Button on Welcome */ +#rules-button { + --btn-bg: linear-gradient(135deg, var(--accent-lemon), #ffcc02); + color: #3e2723; + padding: 10px 16px; + font-size: 14px; + margin-top: 8px; +} + +#rules-button::before { + content: '📋'; + font-size: 14px; + margin-right: 4px; +} + /* ========== Micro-interactions: Ripple ========== */ .ripple { position: absolute; @@ -318,7 +417,8 @@ button:active { /* ========== Page Transition Helpers ========== */ .fade-in { - animation: fadeInUp 420ms cubic-bezier(0.2, 0.75, 0.25, 1) both; + /* Smooth but snappy ~360ms per preference */ + animation: fadeInUp 360ms cubic-bezier(0.2, 0.75, 0.25, 1) both; } .fade-out { animation: fadeOutDown 320ms ease both; @@ -570,16 +670,28 @@ body.light-surface button { 0% { transform: translate(0px, 0px) rotate(var(--r0, 0deg)) scale(var(--s0, 1)); } - 33% { + 25% { + transform: translate( + calc(var(--x1, 0px) * 0.5), + calc(var(--y1, -10px) * 0.5) + ) + rotate(calc(var(--r1, 8deg) * 0.5)) scale(calc(var(--s1, 1.06) * 0.5 + 1)); + } + 50% { transform: translate(var(--x1, 0px), var(--y1, -10px)) rotate(var(--r1, 8deg)) scale(var(--s1, 1.06)); } - 66% { - transform: translate(var(--x2, 0px), var(--y2, 8px)) - rotate(var(--r2, -8deg)) scale(var(--s2, 0.96)); + 75% { + transform: translate( + calc(var(--x2, 0px) + var(--x1, 0px) * 0.5), + calc(var(--y2, 8px) + var(--y1, -10px) * 0.5) + ) + rotate(calc(var(--r2, -8deg) + var(--r1, 8deg) * 0.5)) + scale(calc(var(--s2, 0.96) + var(--s1, 1.06) * 0.5)); } 100% { - transform: translate(0px, 0px) rotate(var(--r0, 0deg)) scale(var(--s0, 1)); + transform: translate(var(--x2, 0px), var(--y2, 8px)) + rotate(var(--r2, -8deg)) scale(var(--s2, 0.96)); } } @@ -728,6 +840,18 @@ body.question-surface #user-interface button { text-shadow: none; } +/* Subtle accent border/glow on question card hover */ +body.question-surface #user-interface.centered:hover { + border-color: color-mix( + in lab, + var(--answer-hover-accent, #ffd93d) 45%, + rgba(255, 255, 255, 0.22) + ); + box-shadow: var(--glass-shadow), + 0 0 0 6px + color-mix(in lab, var(--answer-hover-accent, #ffd93d) 14%, transparent); +} + body.question-surface #user-interface ul li { color: #000 !important; /* answers back to black by request */ } @@ -1036,3 +1160,708 @@ body.question-surface.question-light #user-interface button { margin: 0; /* Container handles centering */ display: block; } + +/* ===== End Page Styling (prize system) ===== */ +.end-page { + text-align: center; + color: #1a1a1a; /* Dark text for professionalism and readability */ + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* Strong shadow for contrast on backgrounds */ +} + +.end-page .end-title, +.end-page .end-subtitle, +.end-page .end-copy { + color: #1a1a1a !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5) !important; + font-weight: 600; + letter-spacing: 0.3px; +} + +.end-page .score-badge { + color: #000 !important; + background: rgba(255, 255, 255, 0.9) !important; + border-color: rgba(0, 0, 0, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.end-page .prize-box { + color: #1a1a1a !important; +} + +.end-page .prize-box .prize-name, +.end-page .prize-box .prize-desc { + color: #1a1a1a !important; + text-shadow: none; +} + +.end-page .end-title { + margin: 0 0 6px; + font-weight: 700; + letter-spacing: 0.3px; + font-size: clamp(1.6rem, 3.5vw, 2.2rem); + color: var(--text-strong); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35), + 0 0 12px + color-mix(in lab, var(--answer-hover-accent, #ffd93d) 55%, transparent); +} + +.end-page .end-subtitle { + margin: 0 0 8px; + color: var(--text-muted); + font-weight: 600; +} + +.end-page .score-badge { + display: inline-block; + padding: 8px 14px; + margin: 6px auto 10px; + border-radius: 999px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.18), + rgba(255, 255, 255, 0.08) + ); + border: 1px solid + color-mix( + in lab, + var(--answer-hover-accent, #ffd93d) 45%, + rgba(255, 255, 255, 0.22) + ); + color: #eaf7ea; + font-weight: 700; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 10px 24px rgba(0, 0, 0, 0.28), + 0 0 0 6px + color-mix(in lab, var(--answer-hover-accent, #ffd93d) 14%, transparent); +} + +.end-page .end-copy { + color: var(--text-muted); + margin: 10px auto 12px; + max-width: 560px; +} + +.end-page .end-reset-btn { + margin-top: 16px; +} + +/* Prize card */ +.prize-box { + width: min(92vw, 560px); + margin: 12px auto 0; + padding: 16px 18px; + border-radius: 16px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.14), + rgba(255, 255, 255, 0.08) + ); + border: 1px solid rgba(255, 255, 255, 0.22); + box-shadow: var(--glass-shadow); + text-align: center; + animation: fadeInUp 420ms cubic-bezier(0.2, 0.75, 0.25, 1) both; +} + +.prize-box .prize-emoji { + font-size: 40px; + line-height: 1; + margin-bottom: 6px; + filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.25)); +} + +.prize-box .prize-name { + font-weight: 700; + letter-spacing: 0.3px; + margin-bottom: 4px; +} + +.prize-box .prize-desc { + color: var(--text-muted); + margin: 0; +} + +/* Accent-tinted glow for prize box on end page */ +.end-page .prize-box { + box-shadow: var(--glass-shadow), + 0 0 0 6px + color-mix(in lab, var(--answer-hover-accent, #ffd93d) 18%, transparent); +} + +.prize-box.prize-none { + border-style: dashed; + opacity: 0.95; +} + +/* Rules Modal Styles */ +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal.show { + display: flex; +} + +.modal-content { + position: relative; + max-width: 400px; + width: 90%; + max-height: 70vh; + overflow-y: auto; + padding: 24px 24px 80px; /* Extra bottom padding to ensure close button visibility */ + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0.06) + ); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 16px; + box-shadow: var(--glass-shadow); + color: #1a1a1a; + text-align: center; +} + +.modal-close { + position: absolute; + top: 12px; + right: 16px; + background: none; + border: 1px solid rgba(0, 0, 0, 0.1); + font-size: 28px; + color: #333; + cursor: pointer; + padding: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 200ms ease; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.modal-close:hover { + background: rgba(211, 47, 47, 0.1); + color: #d32f2f; + border-color: #d32f2f; + transform: scale(1.1); +} + +.modal h2 { + margin: 0 0 16px; + font-size: 1.4rem; + font-weight: 600; + color: #1a1a1a; +} + +.rules-list { + list-style: disc; + padding: 0 0 0 20px; + margin: 0; + text-align: left; +} + +.rules-list li { + padding: 12px 0; + color: #333; + font-size: 16px; + line-height: 1.5; + font-weight: 500; +} + +@media (max-width: 520px) { + .modal-content { + padding: 20px; + margin: 20px; + } +} + +/* Hint Tracker Styles */ +.hint-tracker { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 12px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.14), + rgba(255, 255, 255, 0.08) + ); + border: 1px solid rgba(255, 255, 255, 0.22); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 8px 18px rgba(0, 0, 0, 0.25); + font-size: 12px; + font-weight: 600; + color: #1a1a1a; +} + +.hint-tracker .hint-label { + text-transform: uppercase; + opacity: 0.9; + letter-spacing: 0.5px; +} + +.hint-tracker .hint-used { + font-weight: 700; + color: var(--accent-info); +} + +/* ===== Quiz Header Layout (structured, responsive) ===== */ +.quiz-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} +.quiz-header h1 { + margin: 0; +} + +/* ===== Professional Score Widget ===== */ +.score-widget { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 12px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.14), + rgba(255, 255, 255, 0.08) + ); + border: 1px solid rgba(255, 255, 255, 0.22); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 8px 18px rgba(0, 0, 0, 0.25); +} +.score-widget .score-label { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.5px; + opacity: 0.9; + text-transform: uppercase; +} +.score-widget .score-values { + display: inline-flex; + align-items: baseline; + gap: 6px; + font-weight: 700; +} +.score-widget .score-current { + font-size: 20px; + line-height: 1; + min-width: 1.5ch; + text-align: right; +} +.score-widget .score-sep { + opacity: 0.75; +} +.score-widget .score-total { + opacity: 0.9; +} + +/* Score bump feedback */ +.score-widget.score-bump { + /* Consistent with 300–400ms range */ + animation: scoreBump 360ms cubic-bezier(0.2, 0.75, 0.25, 1); +} +@keyframes scoreBump { + 0% { + transform: translateY(0) scale(1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 8px 18px rgba(0, 0, 0, 0.25); + } + 40% { + transform: translateY(-1px) scale(1.06); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), + 0 14px 28px rgba(0, 0, 0, 0.3); + } + 100% { + transform: translateY(0) scale(1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), + 0 8px 18px rgba(0, 0, 0, 0.25); + } +} + +/* Prize ladder removed */ + +/* ===== Answer Actions Row ===== */ +.actions-row { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} + +/* ===== Hint/Elimination Polished Effects ===== */ +button.hint-used { + --btn-bg: linear-gradient(135deg, #bdbdbd, #9e9e9e) !important; + color: #1a1a1a !important; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25) !important; +} +button.hint-used::before { + content: '🔒'; + transform: translateY(1px); +} + +/* Disabled buttons general tone */ +button:disabled { + opacity: 0.7; + cursor: not-allowed; + filter: saturate(0.9) brightness(0.95); +} + +button.hint-used { + --btn-bg: linear-gradient(135deg, #bdbdbd, #9e9e9e) !important; + color: #1a1a1a !important; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25) !important; + animation: fadeOutDown 300ms ease; +} + +button.hint-used::before { + content: '🔒'; + transform: translateY(1px); +} + +/* Answer elimination animation */ +.eliminate-out { + animation: eliminateOut 420ms cubic-bezier(0.2, 0.75, 0.25, 1) forwards; + transform-origin: center; +} +@keyframes eliminateOut { + 0% { + opacity: 1; + transform: translateX(0) scale(1); + filter: blur(0px); + } + 100% { + opacity: 0; + transform: translateX(8px) scale(0.92); + filter: blur(3px); + } +} + +/* Welcome/Question header spacing fix */ +.quiz-header + #prize-panel { + margin-top: 6px; +} + +/* Reduced motion: disable cosmetic animations */ +@media (prefers-reduced-motion: reduce) { + .score-widget, + .prize-ladder .ladder-item.current, + .eliminate-out { + animation: none !important; + transition: none !important; + } +} + +/* ===== Creative Progress Bar with Prize Marks ===== */ + +/* Progress bar wrapper */ +.progress-wrap { + position: relative; + width: 100%; + margin: 6px 0 10px; +} + +/* Progress marks: per-question "balls" positioned along the bar */ +.progress-marks { + position: absolute; + top: -14px; /* sit neatly above the bar */ + left: 0; + right: 0; + height: 16px; + pointer-events: none; +} +.progress-marks .ball { + position: absolute; + top: 0; + transform: translateX(-50%); + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.55); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); + transition: transform 220ms ease, filter 220ms ease, opacity 220ms ease; + opacity: 0.95; +} +.progress-marks .ball:hover { + transform: translateX(-50%) scale(1.12); + filter: saturate(1.08) brightness(1.04); +} + +/* Ball state colors */ +.progress-marks .ball.state-unanswered { + background: #9e9e9e; + opacity: 0.7; + border-color: rgba(255, 255, 255, 0.4); +} +.progress-marks .ball.state-correct { + background: linear-gradient(180deg, #43a047, #66bb6a); +} +.progress-marks .ball.state-wrong { + background: linear-gradient(180deg, #e53935, #f44336); +} +.progress-marks .ball.state-hinted-only { + background: linear-gradient(180deg, #1976d2, #2196f3); +} +.progress-marks .ball.state-hinted-correct { + background: linear-gradient(90deg, #2196f3 0 50%, #43a047 50% 100%); +} +.progress-marks .ball.state-hinted-wrong { + background: linear-gradient(90deg, #2196f3 0 50%, #e53935 50% 100%); +} +.progress-marks .ball.state-avoided { + background: linear-gradient(180deg, #ff8c00, #ffa62b); +} + +/* Progress bar itself */ +.progress-bar { + position: relative; + width: 100%; + height: 12px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.22); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} +.progress-fill { + height: 100%; + width: 0%; + border-radius: 999px; + /* Rotating accent gradient per question with safe fallbacks */ + background: linear-gradient( + 90deg, + var(--progress-c1, var(--accent-lemon)), + var(--progress-c2, var(--accent-carrot)), + var(--progress-c3, var(--accent-fresh-green)), + var(--progress-c4, var(--accent-tomato)) + ); + /* Progress motion ~360ms */ + transition: width 360ms cubic-bezier(0.2, 0.75, 0.25, 1); + box-shadow: 0 0 10px rgba(107, 203, 119, 0.5); +} +/* ===== Prize System Enhancements (progress, prize pop, salad bowl, confetti) ===== */ + +/* Per-question Prize Pop */ +.prize-pop { + position: relative; /* anchor confetti */ + margin: 8px 0 6px; + min-height: 0; + display: grid; + place-items: center; +} +.prize-pop .prize-pop-card { + display: inline-grid; + place-items: center; + padding: 10px 12px; + border-radius: 14px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.14), + rgba(255, 255, 255, 0.08) + ); + border: 1px solid rgba(255, 255, 255, 0.22); + box-shadow: var(--glass-shadow); + text-align: center; + animation: prizePopIn 520ms cubic-bezier(0.2, 0.75, 0.25, 1); +} +.prize-pop .prize-pop-card .prize-emoji { + font-size: 32px; + line-height: 1; + margin-bottom: 4px; + filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.25)); +} +.prize-pop .prize-pop-card .prize-title { + font-weight: 700; + letter-spacing: 0.3px; +} +.prize-pop .prize-pop-card .prize-msg { + margin: 2px 0 0; + color: var(--text-muted); +} +.prize-pop .prize-pop-card.correct { + box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.18) inset, var(--glass-shadow); +} +.prize-pop .prize-pop-card.fail { + box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.18) inset, var(--glass-shadow); +} +@keyframes prizePopIn { + 0% { + transform: translateY(6px) scale(0.96); + opacity: 0; + } + 60% { + transform: translateY(-2px) scale(1.04); + opacity: 1; + } + 100% { + transform: translateY(0) scale(1); + } +} + +/* Cumulative Salad Bowl */ +.salad-bowl { + display: grid; + grid-auto-flow: row; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 6px; + min-height: 36px; + padding: 8px; + border-radius: 14px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.08), + rgba(255, 255, 255, 0.06) + ); + border: 1px solid rgba(255, 255, 255, 0.18); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} +.salad-bowl .ingredient { + display: grid; + place-items: center; + width: 32px; + height: 32px; + font-size: 20px; + border-radius: 10px; + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); + animation: ingredientPop 520ms cubic-bezier(0.2, 0.75, 0.25, 1); +} +@keyframes ingredientPop { + 0% { + transform: translateY(6px) scale(0.8); + opacity: 0; + } + 60% { + transform: translateY(-2px) scale(1.08); + opacity: 1; + } + 100% { + transform: translateY(0) scale(1); + } +} + +/* Floating salad ingredients on question pages */ +.float-ingredients { + position: fixed; + inset: 0; + z-index: 0; /* keep behind card content */ + pointer-events: none; +} +.float-ingredients .icon { + position: absolute; + top: var(--top, 50%); + left: var(--left, 50%); + transform: translate(-50%, -50%); + animation-name: leafRandom; + animation-duration: var(--dur, 4s); + animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); + animation-iteration-count: infinite; + animation-delay: var(--delay, 0s); + filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.25)) + drop-shadow( + 0 0 8px + color-mix(in lab, var(--answer-hover-accent, #ffd93d) 35%, transparent) + ); + color: color-mix( + in lab, + var(--answer-hover-accent, #ffd93d) 80%, + transparent + ); + transform-origin: center; +} + +/* Answer feedback animations */ +.correct-bounce { + animation: correctBounce 520ms cubic-bezier(0.2, 0.75, 0.25, 1); +} +@keyframes correctBounce { + 0% { + transform: scale(0.98); + } + 50% { + transform: scale(1.06); + } + 100% { + transform: scale(1); + } +} +.incorrect-wilt { + animation: incorrectWilt 520ms ease; +} +@keyframes incorrectWilt { + 0% { + transform: none; + filter: none; + } + 100% { + transform: rotate(-2deg) scale(0.96); + filter: grayscale(0.2) brightness(0.9); + } +} + +/* Missing keyframes for .shake (used on errors and hint feedback) */ +@keyframes incorrectShake { + 0% { + transform: translateX(0); + } + 20% { + transform: translateX(-4px); + } + 40% { + transform: translateX(4px); + } + 60% { + transform: translateX(-3px); + } + 80% { + transform: translateX(3px); + } + 100% { + transform: translateX(0); + } +} + +/* Confetti effect anchored to prize-pop */ +.confetti { + position: absolute; + inset: 0; + pointer-events: none; + overflow: visible; +} +.confetti-piece { + position: absolute; + top: 0; + width: 6px; + height: 10px; + left: 50%; + transform: translateX(-50%); + border-radius: 2px; + background: linear-gradient(180deg, #ffeb3b, #66bb6a); + opacity: 0.95; + animation: confettiFall 1.1s ease-out forwards; +} +@keyframes confettiFall { + 0% { + transform: translateY(-8px) rotate(0deg); + opacity: 0; + } + 10% { + opacity: 1; + } + 100% { + transform: translateY(140px) rotate(320deg); + opacity: 0; + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..63de480 --- /dev/null +++ b/server.js @@ -0,0 +1,35 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const server = http.createServer((req, res) => { + let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url); + const ext = path.extname(filePath); + let contentType = 'text/html'; + + switch (ext) { + case '.js': + contentType = 'text/javascript'; + break; + case '.css': + contentType = 'text/css'; + break; + case '.ico': + contentType = 'image/x-icon'; + break; + } + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end('File not found'); + return; + } + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); +}); + +server.listen(3000, () => { + console.log('Server running at http://localhost:3000'); +}); diff --git a/src/app.js b/src/app.js index 54083e6..13b2f97 100644 --- a/src/app.js +++ b/src/app.js @@ -5,6 +5,9 @@ import { START_QUIZ_BUTTON_ID, NEXT_QUESTION_BUTTON_ID, AVOID_QUESTION_BUTTON_ID, + STORAGE_KEY, + ACCENT_CYCLING_ENABLED, + DEFAULT_ACCENT_NAME, } from './constants.js'; /** @@ -13,9 +16,28 @@ import { * - Shows welcome page */ const loadApp = () => { - resetQuizState(); setupUIEnhancements(); - // Welcome background on initial load + try { + setEmojiFavicon('🥗'); + } catch {} + + // Try to hydrate saved progress; if available resume where the user left off + const hydrated = hydrateFromStorage(); + + if (hydrated) { + const idx = quizData.currentQuestionIndex; + if (idx >= quizData.questions.length) { + changeBackground(999); + import('./pages/endPage.js').then((m) => m.showEndPage()); + } else { + changeBackground(Math.max(0, idx)); + import('./pages/questionPage.js').then((m) => m.initQuestionPage()); + } + return; + } + + // No saved state -> fresh start + resetQuizState(); changeBackground(-1); initWelcomePage(); }; @@ -25,6 +47,20 @@ const loadApp = () => { * - currentQuestionIndex back to 0 * - clears all selected answers */ +export const resetQuizState = () => { + quizData.currentQuestionIndex = 0; + quizData.questions.forEach((q) => { + q.selected = null; + q.usedHint = false; + q.avoided = false; + }); + quizData.hintsLeft = 3; + try { + console.log('score:', quizData.score()); + } catch { + /* ignore */ + } +}; /** * Move to next question @@ -39,13 +75,107 @@ export const goToNextQuestion = () => { return false; }; -export const resetQuizState = () => { - quizData.currentQuestionIndex = 0; - quizData.questions.forEach((q) => (q.selected = null)); - quizData.score = 0; - console.log('score: ', quizData.score); - initWelcomePage(); -}; +/** + * Persistence helpers + * - saveState(): persist minimal quiz state to localStorage + * - loadState(): read persisted state or return null + * - clearState(): remove persisted data + * - hydrateFromStorage(): apply persisted data into quizData, return true if applied + */ +export function saveState() { + try { + const payload = { + userName: quizData.userName || '', + currentQuestionIndex: quizData.currentQuestionIndex, + hintsLeft: typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3, + selectedMap: Object.fromEntries( + quizData.questions.map((q) => [q.id, q.selected ?? null]) + ), + usedHintMap: Object.fromEntries( + quizData.questions.map((q) => [q.id, !!q.usedHint]) + ), + avoidedMap: Object.fromEntries( + quizData.questions.map((q) => [q.id, !!q.avoided]) + ), + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); + } catch (e) { + console.warn('saveState failed:', e); + } +} + +export function clearState() { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + /* ignore */ + } +} + +export function loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw); + } catch { + return null; + } +} + +function hydrateFromStorage() { + const state = loadState(); + if (!state || !Array.isArray(quizData.questions)) return false; + + if (typeof state.userName === 'string') { + quizData.userName = state.userName; + } + if (typeof state.hintsLeft === 'number') { + quizData.hintsLeft = Math.max(0, Math.min(3, state.hintsLeft)); + } + + if (typeof state.currentQuestionIndex === 'number') { + quizData.currentQuestionIndex = Math.max( + 0, + Math.min(state.currentQuestionIndex, quizData.questions.length) + ); + } + + if (state.selectedMap && typeof state.selectedMap === 'object') { + for (const q of quizData.questions) { + if ( + q && + q.id && + Object.prototype.hasOwnProperty.call(state.selectedMap, q.id) + ) { + q.selected = state.selectedMap[q.id]; + } + } + } + if (state.usedHintMap && typeof state.usedHintMap === 'object') { + for (const q of quizData.questions) { + if ( + q && + q.id && + Object.prototype.hasOwnProperty.call(state.usedHintMap, q.id) + ) { + q.usedHint = !!state.usedHintMap[q.id]; + } + } + } + if (state.avoidedMap && typeof state.avoidedMap === 'object') { + for (const q of quizData.questions) { + if ( + q && + q.id && + Object.prototype.hasOwnProperty.call(state.avoidedMap, q.id) + ) { + q.avoided = !!state.avoidedMap[q.id]; + } + } + } + + return true; +} /** * UI Enhancement helpers (styling & transitions only) @@ -117,11 +247,33 @@ export function changeBackground(index) { document.documentElement.style.setProperty('--bg-gradient', gradient); // Toggle high-contrast text when the background is very light - // We consider the 4th gradient (index 3, with yellow) and the end screen (999) as "light" const isLightSurface = index === 999 || (typeof index === 'number' && Math.abs(index) % gradients.length === 3); document.body.classList.toggle('light-surface', !!isLightSurface); + + // Also set a sensible accent for non-question screens so glow effects feel cohesive + try { + const root = document.documentElement; + const get = (name, fallback) => + getComputedStyle(root).getPropertyValue(name).trim() || fallback; + const accents = [ + get('--accent-lemon', '#FFD93D'), + get('--accent-carrot', '#FFA62B'), + get('--accent-fresh-green', '#6BCB77'), + get('--accent-tomato', '#FF6B6B'), + ]; + if (index === -1) { + // welcome: fresh, inviting + root.style.setProperty('--answer-hover-accent', accents[2]); + } else if (index === 999) { + // end: celebratory + root.style.setProperty('--answer-hover-accent', accents[0]); + } else if (typeof index === 'number') { + const accent = accents[Math.abs(index) % accents.length]; + root.style.setProperty('--answer-hover-accent', accent); + } + } catch {} } /** @@ -194,18 +346,18 @@ function observeUI() { ) { // Welcome changeBackground(-1); - } else if ( - node.querySelector && - node.querySelector('#restart-quiz-button') - ) { - // End - changeBackground(999); + try { + setEmojiFavicon('🥗'); + } catch {} } else if ( node.querySelector && node.querySelector(`#${NEXT_QUESTION_BUTTON_ID}`) ) { // Question changeBackground(quizData.currentQuestionIndex); + try { + setEmojiFavicon('🥬'); + } catch {} } }); } @@ -222,7 +374,6 @@ function setupUIEnhancements() { observeUI(); } -// Question page theming: rotate through salad colors and manage contrast // Question page theming: rotate through salad colors and manage contrast export function setQuestionTheme(index) { const gradients = [ @@ -247,6 +398,34 @@ export function setQuestionTheme(index) { chosen.includes('#FFFDE7') || chosen.includes('#FFFFFF'); document.body.classList.toggle('question-light', !!isLight); + + // Cycle answer hover accent per question for playful variety + try { + const root = document.documentElement; + const get = (name, fallback) => + getComputedStyle(root).getPropertyValue(name).trim() || fallback; + const accents = [ + get('--accent-lemon', '#FFD93D'), + get('--accent-carrot', '#FFA62B'), + get('--accent-fresh-green', '#6BCB77'), + get('--accent-tomato', '#FF6B6B'), + ]; + const accent = accents[Math.abs(index) % accents.length]; + root.style.setProperty('--answer-hover-accent', accent); + + // Rotate progress gradient colors per question for subtle variety + const r0 = Math.abs(index) % accents.length; + const seq = [ + accents[r0], + accents[(r0 + 1) % accents.length], + accents[(r0 + 2) % accents.length], + accents[(r0 + 3) % accents.length], + ]; + root.style.setProperty('--progress-c1', seq[0]); + root.style.setProperty('--progress-c2', seq[1]); + root.style.setProperty('--progress-c3', seq[2]); + root.style.setProperty('--progress-c4', seq[3]); + } catch {} } /** diff --git a/src/constants.js b/src/constants.js index a7d1be6..6eb4d61 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,14 +1,62 @@ /* - The constants file is used to store anything + The constants file is used to store anything that multiple files use, that should ALWAYS be the same It is an industry standard to make these variables fully capitalised -*/ + */ export const USER_INTERFACE_ID = 'user-interface'; export const START_QUIZ_BUTTON_ID = 'start-quiz-button'; export const ANSWERS_LIST_ID = 'answers-list'; export const NEXT_QUESTION_BUTTON_ID = 'next-question-button'; export const AVOID_QUESTION_BUTTON_ID = 'avoid-question-button'; -export const ELEMINATE_TWO_ANSWERS_BUTTON_ID = 'eliminate-two-answers-button'; -export const RESTART_QUIZ = 'restart-quiz'; +export const ELIMINATE_TWO_ANSWERS_BUTTON_ID = 'eliminate-two-answers-button'; + +export const SCORE_INDICATOR_ID = 'score-indicator'; +export const STORAGE_KEY = 'quiz-state-v1'; + +/* Prize ladder removed */ + +/* New UI IDs for interactive prize experience */ +export const PROGRESS_BAR_ID = 'progress-bar'; +export const PROGRESS_FILL_ID = 'progress-fill'; +export const PROGRESS_MARKS_ID = 'progress-marks'; +export const SALAD_BOWL_ID = 'salad-bowl'; +export const PRIZE_POP_ID = 'prize-pop'; + +/* Money prize tiers removed */ + +/* Salad-themed prize progression (stage 1..10) */ +export const PRIZE_STEPS = [ + { name: 'Lettuce Leaf', emoji: '🥬' }, + { name: 'Cherry Tomato', emoji: '🍅' }, + { name: 'Cucumber Slice', emoji: '🥒' }, + { name: 'Crunchy Crouton', emoji: '🥖' }, + { name: 'Olive Ring', emoji: '🫒' }, + { name: 'Cheese Star', emoji: '🧀' }, + { name: 'Dressing Drizzle', emoji: '🫙' }, + { name: 'Salad Bowl Crown', emoji: '🥗' }, + { name: 'Golden Fork', emoji: '🍴' }, + { name: 'Salad Master Trophy', emoji: '🏆' }, +]; + +/* Ingredients added to the salad bowl on each correct answer */ +export const SALAD_BOWL_INGREDIENTS = [ + '🥬', + '🍅', + '🥒', + '🥖', + '🫒', + '🧀', + '🫙', + '🧅', + '🥑', + '🥗', +]; + +// Theming configuration +// Toggle whether per-question accent cycling is enabled. If false, a single default accent is used. +export const ACCENT_CYCLING_ENABLED = true; +// Name of the CSS variable to use as the default accent when cycling is disabled +// Must correspond to one of: '--accent-lemon', '--accent-carrot', '--accent-fresh-green', '--accent-tomato' +export const DEFAULT_ACCENT_NAME = '--accent-fresh-green'; diff --git a/src/data.js b/src/data.js index b107ddd..701c501 100644 --- a/src/data.js +++ b/src/data.js @@ -9,7 +9,8 @@ export const quizData = { currentQuestionIndex: 0, - userName: '', //store user name + userName: '', // store user name + hintsLeft: 3, // eliminate-two-answers uses remaining across the whole quiz // All quiz questions questions: [ // EASY diff --git a/src/pages/endPage.js b/src/pages/endPage.js index 35dedc0..71ef563 100644 --- a/src/pages/endPage.js +++ b/src/pages/endPage.js @@ -1,29 +1,114 @@ // src/pages/endPage.js -import { USER_INTERFACE_ID } from '../constants.js'; -import { quizData } from '../data.js'; -import { changeBackground } from '../app.js'; import { createPage } from '../utils/createPage.js'; +import { changeBackground, resetQuizState, clearState } from '../app.js'; +import { USER_INTERFACE_ID, PRIZE_STEPS } from '../constants.js'; +import { quizData } from '../data.js'; -// Show the end page export const showEndPage = () => { changeBackground(999); const userInterface = document.getElementById(USER_INTERFACE_ID); userInterface.innerHTML = ''; - // compute score for logic, but do not render it (UI stays 0/total) - const _score = quizData.score(); + const total = quizData.questions.length; + const score = quizData.score(); + const { headline, subline } = buildEndCopy(score, total, quizData.userName); + const prize = getPrize(score, total); + + const prizeHtml = prize + ? ` +
+
${prize.emoji}
+
${prize.name}
+

${prize.desc}

+
+ ` + : ` +
+
🥲
+
No Prize
+

3+ wrong answers locked the prize vault. Try again!

+
+ `; const endElement = createPage( 'end-page', ` -

Quiz Completed!

-

Congratulations, ${quizData.userName}!

-
Score: 0 / ${quizData.questions.length}
- - ` +

Quiz Completed!

+

${headline}

+
Score: ${score} / ${total}
+

${subline}

+ ${prizeHtml} + + ` ); userInterface.appendChild(endElement); - // UI-only Reset button (no behavior by request) + // Reset quiz and go back to welcome + const playAgainBtn = document.getElementById('play-again-button'); + if (playAgainBtn) { + playAgainBtn.addEventListener('click', async () => { + clearState(); + resetQuizState(); + changeBackground(-1); + const module = await import('./welcomePage.js'); + module.initWelcomePage(); + }); + } }; + +function buildEndCopy(score, total, name) { + const user = name || 'Champion'; + + if (score === total) { + return { + headline: `Flawless victory, ${user}!`, + subline: + 'You answered everything correctly. Are you secretly a quiz AI? 🏆', + }; + } + if (score >= total - 1) { + return { + headline: `So close to legend status, ${user}!`, + subline: "One more and we'd rename the quiz after you. ⭐️", + }; + } + if (score >= total - 2) { + return { + headline: `Strong run, ${user}!`, + subline: 'Two answers shy of eternal bragging rights.', + }; + } + if (score >= Math.ceil(total * 0.6)) { + return { + headline: `Solid performance, ${user}!`, + subline: 'The scoreboard approves. The crowd goes mild. 👏', + }; + } + if (score >= Math.ceil(total * 0.3)) { + return { + headline: `Nice attempt, ${user}!`, + subline: 'You showed sparks. Recharge and give it another go.', + }; + } + if (score >= 1) { + return { + headline: `A spark of genius, ${user}!`, + subline: 'Every epic starts with one correct click. Keep going. ⚡️', + }; + } + return { + headline: `We saw nothing, ${user}.`, + subline: 'The scoreboard remains mysterious. Try again!', + }; +} + +function getPrize(score, total) { + const wrongCount = total - score; + if (wrongCount >= 3) { + return null; // No prize if 3+ wrong answers + } + + const prizeIndex = Math.min(score - 1, PRIZE_STEPS.length - 1); + return prizeIndex >= 0 ? PRIZE_STEPS[prizeIndex] : null; +} diff --git a/src/pages/questionPage.js b/src/pages/questionPage.js index 4ac9b00..a182d87 100644 --- a/src/pages/questionPage.js +++ b/src/pages/questionPage.js @@ -3,112 +3,537 @@ import { NEXT_QUESTION_BUTTON_ID, USER_INTERFACE_ID, AVOID_QUESTION_BUTTON_ID, - ELEMINATE_TWO_ANSWERS_BUTTON_ID, - RESTART_QUIZ, + ELIMINATE_TWO_ANSWERS_BUTTON_ID, + SCORE_INDICATOR_ID, + PROGRESS_BAR_ID, + PROGRESS_FILL_ID, + PROGRESS_MARKS_ID, + SALAD_BOWL_ID, + PRIZE_POP_ID, + PRIZE_STEPS, + SALAD_BOWL_INGREDIENTS, } from '../constants.js'; import { createQuestionElement } from '../views/questionView.js'; import { createAnswerElement } from '../views/answerView.js'; import { quizData } from '../data.js'; -import { resetQuizState } from '../app.js'; -import { showEndPage } from './endPage.js'; -import { setQuestionTheme, resetQuestionTheme } from '../app.js'; +import { + setQuestionTheme, + resetQuestionTheme, + saveState, + fadeTransition, + clearState, + resetQuizState, + changeBackground, +} from '../app.js'; + +// Makes the number change smoothly from old value to new value +function animateNumber(el, from, to, duration = 450, formatter) { + const start = performance.now(); // Start time for animation + const clamp = (v) => (to >= from ? Math.min(v, to) : Math.max(v, to)); // Keeps value between from and to + const f = + typeof formatter === 'function' ? formatter : (v) => String(Math.round(v)); // Formats the number + function frame(now) { + // Runs each frame to update number + const t = Math.min(1, (now - start) / duration); // Progress from 0 to 1 + const eased = 1 - Math.pow(1 - t, 3); // Smooth easing for animation + const value = clamp(from + (to - from) * eased); // Current value during animation + el.textContent = f(value); // Shows the value + el.dataset.value = String(to); // Stores final value + if (t < 1) requestAnimationFrame(frame); // Continues animation if not done + } + requestAnimationFrame(frame); // Starts the animation +} + +// Money-based prize removed (no currency display) + +// Money-based earnings removed + +// Counts how many questions user answered +function countAnswered() { + return quizData.questions.reduce((n, q) => n + (q.selected ? 1 : 0), 0); // Adds 1 for each selected answer +} + +// Counts how many wrong answers user has +function countWrong() { + return Math.max(0, countAnswered() - quizData.score()); // Wrong = answered minus correct score +} + +// Updates the progress bar to show how many questions done +function updateProgressBar() { + const fill = document.getElementById(PROGRESS_FILL_ID); // Gets the progress fill element + if (!fill) return; // Stops if no element + const total = quizData.questions.length || 0; // Total questions + const answered = countAnswered(); // How many answered + const pct = total ? Math.round((answered / total) * 100) : 0; // Percentage done + fill.style.width = pct + '%'; // Sets bar width + fill.setAttribute('aria-valuenow', String(pct)); // Accessibility update + // Updates prize marks too + try { + updateProgressMarks(); + } catch {} +} + +// Updates the salad bowl to show prizes from correct answers +function updateSaladBowl() { + const bowl = document.getElementById(SALAD_BOWL_ID); // Gets the bowl element + if (!bowl) return; // Stops if no element + const correct = quizData.score(); // Number of correct answers + const items = SALAD_BOWL_INGREDIENTS.slice(0, correct) // Takes items up to correct count + .map( + (emoji, i) => `${emoji}` + ) // Makes HTML for each + .join(''); // Joins them + bowl.innerHTML = items; // Shows in bowl +} + +// Builds marks on progress bar for each prize step +function buildProgressMarks() { + const cont = document.getElementById(PROGRESS_MARKS_ID); + if (!cont) return; + const total = quizData.questions.length || 0; + cont.innerHTML = ''; + if (total <= 0) return; + + for (let i = 0; i < total; i++) { + const dot = document.createElement('span'); + dot.className = 'ball'; + const left = total <= 1 ? 0 : (i / (total - 1)) * 100; + dot.style.left = left + '%'; + dot.dataset.index = String(i); + cont.appendChild(dot); + } + updateProgressMarks(); +} -// Step 1: Store selected answer +// Updates which marks are done or current +function updateProgressMarks() { + const cont = document.getElementById(PROGRESS_MARKS_ID); + if (!cont) return; + const dots = cont.querySelectorAll('.ball'); + const total = quizData.questions.length || 0; + + const STATES = [ + 'state-unanswered', + 'state-correct', + 'state-wrong', + 'state-hinted-only', + 'state-hinted-correct', + 'state-hinted-wrong', + 'state-avoided', + ]; + + dots.forEach((dot, idx) => { + if (idx >= total) return; + const q = quizData.questions[idx]; + + // Determine state + let cls = 'state-unanswered'; + let title = 'Unanswered'; + + if (q) { + if (q.avoided) { + cls = 'state-avoided'; + title = 'Avoided'; + } else if (q.selected == null) { + if (q.usedHint) { + cls = 'state-hinted-only'; + title = 'Hint used'; + } else { + cls = 'state-unanswered'; + title = 'Unanswered'; + } + } else { + const isCorrect = q.selected === q.correct; + if (q.usedHint && isCorrect) { + cls = 'state-hinted-correct'; + title = 'Hint + Correct'; + } else if (q.usedHint && !isCorrect) { + cls = 'state-hinted-wrong'; + title = 'Hint + Wrong'; + } else if (!q.usedHint && isCorrect) { + cls = 'state-correct'; + title = 'Correct'; + } else { + cls = 'state-wrong'; + title = 'Wrong'; + } + } + } + + // Reset and apply classes + STATES.forEach((s) => dot.classList.remove(s)); + dot.classList.add(cls); + dot.title = title; + }); +} + +// Adds floating salad items in background for each question +function ensureFloatingIngredients() { + const ui = document.getElementById(USER_INTERFACE_ID); // Gets main UI + if (!ui) return; // Stops if no UI + if (ui.querySelector('.float-ingredients')) return; // Stops if already added + const cont = document.createElement('div'); // New container for floats + cont.className = 'float-ingredients'; // Adds class + const emojis = ['🥬', '🍅', '🥖', '🥒', '🫒']; // List of salad emojis + for (let i = 0; i < 10; i++) { + // Makes 10 floating items + const span = document.createElement('span'); // New item + span.className = 'icon'; // Adds class + span.textContent = emojis[i % emojis.length]; // Random emoji + const top = (10 + Math.random() * 80).toFixed(2) + '%'; // Random top position + const left = (5 + Math.random() * 90).toFixed(2) + '%'; // Random left position + const dur = (2.4 + Math.random() * 2.2).toFixed(2) + 's'; // Random duration + const delay = (Math.random() * 1.2).toFixed(2) + 's'; // Random delay + const x1 = (Math.random() * 16 - 8).toFixed(0) + 'px'; // X move 1 + const y1 = (Math.random() * 12 - 6).toFixed(0) + 'px'; // Y move 1 + const x2 = (Math.random() * 16 - 8).toFixed(0) + 'px'; // X move 2 + const y2 = (Math.random() * 12 - 6).toFixed(0) + 'px'; // Y move 2 + span.style.setProperty('--dur', dur); // Sets duration + span.style.setProperty('--delay', delay); // Sets delay + span.style.setProperty('--x1', x1); // Sets X1 + span.style.setProperty('--y1', y1); // Sets Y1 + span.style.setProperty('--x2', x2); // Sets X2 + span.style.setProperty('--y2', y2); // Sets Y2 + span.style.setProperty('--r0', (Math.random() * 6 - 3).toFixed(1) + 'deg'); // Rotation 0 + span.style.setProperty('--r1', (Math.random() * 10 - 5).toFixed(1) + 'deg'); // Rotation 1 + span.style.setProperty('--r2', (Math.random() * 10 - 5).toFixed(1) + 'deg'); // Rotation 2 + span.style.setProperty('--s0', (0.9 + Math.random() * 0.2).toFixed(2)); // Scale 0 + span.style.setProperty('--s1', (0.94 + Math.random() * 0.18).toFixed(2)); // Scale 1 + span.style.setProperty('--s2', (0.9 + Math.random() * 0.2).toFixed(2)); // Scale 2 + span.style.setProperty('--top', top); // Sets top + span.style.setProperty('--left', left); // Sets left + cont.appendChild(span); // Adds item + } + // Puts behind card content + ui.prepend(cont); +} + +// Adds confetti when correct answer +function addConfetti() { + const container = document.getElementById(PRIZE_POP_ID) || document.body; // Where to put confetti + const conf = document.createElement('div'); // Confetti container + conf.className = 'confetti'; // Adds class + const pieces = 24; // Number of pieces + for (let i = 0; i < pieces; i++) { + // Makes each piece + const p = document.createElement('i'); // New piece + p.className = 'confetti-piece'; // Adds class + const left = Math.random() * 100; // Random start position + const delay = Math.random() * 0.3; // Random delay + const dur = 0.9 + Math.random() * 0.6; // Random duration + const hue = 40 + Math.random() * 80; // Random color + p.style.left = left.toFixed(2) + '%'; // Sets position + p.style.animationDelay = delay.toFixed(2) + 's'; // Sets delay + p.style.animationDuration = dur.toFixed(2) + 's'; // Sets duration + p.style.filter = `hue-rotate(${hue.toFixed(0)}deg)`; // Sets color + conf.appendChild(p); // Adds piece + } + container.appendChild(conf); // Adds confetti + setTimeout(() => conf.remove(), 1600); // Removes after 1.6 seconds +} + +// Shows prize pop message for correct or wrong answer +function showPrizePop(isCorrect) { + const host = document.getElementById(PRIZE_POP_ID); // Gets prize area + if (!host) return; // Stops if no area + if (!isCorrect) { + // If wrong answer + // If 3 or more wrong, show lock message + if (countWrong() >= 3) { + const who = quizData.userName || 'Player'; // User name or default + host.innerHTML = ` +
+
🥲
+
Prize Vault Locked
+

Oh no, ${who}! 3+ wrong answers means no more prizes this run.

+
`; // Fail message HTML + host.classList.add('prize-pop--show'); // Shows it + setTimeout(() => host.classList.remove('prize-pop--show'), 1400); // Hides after 1.4s + } else { + host.innerHTML = ''; // Clears if less than 3 wrong + } + return; + } + + const correctCount = quizData.score(); // How many correct + const idx = Math.max(0, Math.min(correctCount - 1, PRIZE_STEPS.length - 1)); // Prize index + const step = PRIZE_STEPS[idx] || { name: 'Salad Surprise', emoji: '🥗' }; // Prize info + const who = quizData.userName || 'Player'; // User name + host.innerHTML = ` +
+
${step.emoji}
+
${step.name}
+

Wow ${who}, you tossed that salad perfectly! 🥗 You got ${correctCount} correct.

+
`; // Success message HTML + host.classList.add('prize-pop--show'); // Shows it + addConfetti(); // Adds confetti + setTimeout(() => host.classList.remove('prize-pop--show'), 1400); // Hides after 1.4s +} + +// Ladder UI removed: we now use creative emoji progress marks along the bar + +// refreshPrizePanel removed (no ladder) + +// Updates score display with smooth animation +const updateScoreIndicator = (prevScore) => { + const el = document.getElementById(SCORE_INDICATOR_ID); // Gets score element + if (!el) return; // Stops if no element + const total = quizData.questions.length; // Total questions + const current = quizData.score(); // Current score + + const currentEl = el.querySelector('.score-current'); // Current score part + const totalEl = el.querySelector('.score-total'); // Total part + if (totalEl) totalEl.textContent = String(total); // Updates total + + if (currentEl) { + // If current element exists + const from = + typeof prevScore === 'number' + ? prevScore + : Number(currentEl.dataset.value || currentEl.textContent || 0); // Old score + const to = current; // New score + animateNumber(currentEl, from, to, 450); // Animates change + if (to > from) { + // If score increased + el.classList.remove('score-bump'); // Removes bump class + void el.offsetWidth; // Forces reflow + el.classList.add('score-bump'); // Adds bump animation + } + } else { + el.textContent = `Score: ${current} / ${total}`; // Simple text update + } +}; + +// Saves the chosen answer for a question const storeAnswer = (questionIndex, selectedOption) => { - quizData.questions[questionIndex].selected = selectedOption; - console.log(`Question ${questionIndex + 1} selected:`, selectedOption); + const prevScore = quizData.score(); // Old score before change + const wasCorrect = + selectedOption === quizData.questions[questionIndex].correct; // Checks if correct + + quizData.questions[questionIndex].selected = selectedOption; // Saves choice + console.log(`Question ${questionIndex + 1} selected:`, selectedOption); // Logs choice + + // Saves state and updates displays + try { + saveState(); + } catch {} + updateScoreIndicator(prevScore); // Updates score + updateProgressBar(); // Updates progress + updateSaladBowl(); // Updates bowl + showPrizePop(wasCorrect); // Shows prize or fail }; export const initQuestionPage = () => { - const userInterface = document.getElementById(USER_INTERFACE_ID); - userInterface.innerHTML = ''; + const userInterface = document.getElementById(USER_INTERFACE_ID); // Gets main UI + userInterface.classList.remove('welcome-mode'); // Removes welcome class - // Initializes the question page by rendering the current question and answers - // Handles click events on answers to store selection and show correct/incorrect feedback + // Checks if all questions done, shows end page if yes if (quizData.currentQuestionIndex >= quizData.questions.length) { - resetQuestionTheme(); // leaving question surface - showEndPage(); // Show the end-of-quiz page if all questions are answered + resetQuestionTheme(); // Resets background theme + // Loads end page + import('./endPage.js').then((mod) => mod.showEndPage()); return; } - const currentQuestion = quizData.questions[quizData.currentQuestionIndex]; + userInterface.innerHTML = ''; // Clears UI + + const currentQuestion = quizData.questions[quizData.currentQuestionIndex]; // Current question - // Apply salad-themed background for this question and manage contrast + // Sets background theme for this question setQuestionTheme(quizData.currentQuestionIndex); - const questionElement = createQuestionElement(currentQuestion.text); + const scoreText = `Score: ${quizData.score()} / ${quizData.questions.length}`; // Score text + const questionElement = createQuestionElement( + currentQuestion.text, + scoreText + ); // Makes question HTML + // Adds question to UI userInterface.appendChild(questionElement); - const answersListElement = document.getElementById(ANSWERS_LIST_ID); + // Adds floating items and updates displays + ensureFloatingIngredients(); // Floating salad + buildProgressMarks(); // Progress marks + updateProgressBar(); // Progress bar + updateSaladBowl(); // Salad bowl + + // Updates score display + updateScoreIndicator(); + + // Updates hint count shown + const tracker = document.getElementById('hint-tracker'); // Hint tracker + if (tracker) { + const used = 3 - (quizData.hintsLeft || 3); // How many used + tracker.innerHTML = ` + Hints + ${used}/3 + `; + } - // Render each answer + const answersListElement = document.getElementById(ANSWERS_LIST_ID); // Answers list + + // Makes and adds each answer option for (const [key, answerText] of Object.entries(currentQuestion.answers)) { - const answerElement = createAnswerElement(key, answerText); + const answerElement = createAnswerElement(key, answerText); // Makes answer HTML - // tag each
  • with its answer key + // Tags answer with its key answerElement.dataset.key = key; - // Store answer when clicked + // Handles click on answer answerElement.addEventListener('click', (event) => { - const clickedLi = event.currentTarget; - const selectedKey = clickedLi.dataset.key; + const clickedLi = event.currentTarget; // Clicked item + const selectedKey = clickedLi.dataset.key; // Key of selected - // store selection - storeAnswer(quizData.currentQuestionIndex, key); + // Saves the answer + storeAnswer(quizData.currentQuestionIndex, selectedKey); - // get all
  • within this answers list only + // Gets all answers in list const allListItems = answersListElement.querySelectorAll('li'); - // reset any previous coloring + // Clears old colors allListItems.forEach((li) => { li.style.backgroundColor = ''; }); - // check if user clicked the correct answer + // Checks if correct const isCorrect = selectedKey === currentQuestion.correct; if (isCorrect) { - clickedLi.style.backgroundColor = 'green'; + // If correct + clickedLi.style.backgroundColor = 'green'; // Green color + clickedLi.classList.add('correct-bounce'); // Bounce animation } else { - clickedLi.style.backgroundColor = 'red'; + // If wrong + clickedLi.style.backgroundColor = 'red'; // Red color + clickedLi.classList.add('incorrect-wilt'); // Wilt animation - //highlight the correct answer + // Shows correct one allListItems.forEach((li) => { if (li.dataset.key === currentQuestion.correct) { li.style.backgroundColor = 'green'; } }); } - const nextBtnEl = document.getElementById(NEXT_QUESTION_BUTTON_ID); + const nextBtnEl = document.getElementById(NEXT_QUESTION_BUTTON_ID); // Next button if (nextBtnEl) { - nextBtnEl.classList.remove('btn-error', 'shake'); + nextBtnEl.classList.remove('btn-error', 'shake'); // Clears error } allListItems.forEach((li) => { - li.style.pointerEvents = 'none'; + li.style.pointerEvents = 'none'; // No more clicks }); - // disabled - document.getElementById(AVOID_QUESTION_BUTTON_ID).disabled = true; - document.getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID).disabled = true; + // Disables other buttons + const avoid = document.getElementById(AVOID_QUESTION_BUTTON_ID); + if (avoid) avoid.disabled = true; + const eliminate = document.getElementById( + ELIMINATE_TWO_ANSWERS_BUTTON_ID + ); + if (eliminate) eliminate.disabled = true; }); - answersListElement.appendChild(answerElement); + answersListElement.appendChild(answerElement); // Adds answer } - document - .getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID) - .addEventListener('click', () => { - const allListItems = Array.from( - answersListElement.querySelectorAll('li') - ); - hint(currentQuestion, allListItems); - document.getElementById(ELEMINATE_TWO_ANSWERS_BUTTON_ID).disabled = true; + // If answer already chosen before, shows it + if (currentQuestion.selected) { + const allListItems = answersListElement.querySelectorAll('li'); // All answers + const selectedKey = currentQuestion.selected; // Selected key + allListItems.forEach((li) => { + li.style.backgroundColor = ''; // Clears colors + }); + const isCorrect = selectedKey === currentQuestion.correct; // Checks correct + // Colors selected and correct + allListItems.forEach((li) => { + if (li.dataset.key === selectedKey) { + li.style.backgroundColor = isCorrect ? 'green' : 'red'; // Selected color + } + if (li.dataset.key === currentQuestion.correct) { + li.style.backgroundColor = 'green'; // Correct color + } + li.style.pointerEvents = 'none'; // No clicks + }); + // Disables buttons + const avoid = document.getElementById(AVOID_QUESTION_BUTTON_ID); + if (avoid) avoid.disabled = true; + const eliminate = document.getElementById(ELIMINATE_TWO_ANSWERS_BUTTON_ID); + if (eliminate) eliminate.disabled = true; + + // Clears next button error + const nextBtnEl = document.getElementById(NEXT_QUESTION_BUTTON_ID); + if (nextBtnEl) { + nextBtnEl.classList.remove('btn-error', 'shake'); + } + updateScoreIndicator(); // Updates score + updateProgressBar(); // Updates progress + updateSaladBowl(); // Updates bowl for consistency + } + + const eliminateBtn = document.getElementById(ELIMINATE_TWO_ANSWERS_BUTTON_ID); + if (eliminateBtn) { + let hintUsed = false; + + const refreshEliminateUI = () => { + const hintsLeft = typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3; + eliminateBtn.textContent = 'Hint'; + const shouldDisable = hintUsed || !!currentQuestion.selected; + eliminateBtn.disabled = shouldDisable; + if (hintUsed) { + eliminateBtn.classList.add('hint-used'); + } else { + eliminateBtn.classList.remove('hint-used'); + } + }; + + // initial state for this question + refreshEliminateUI(); + + eliminateBtn.addEventListener('click', () => { + if (eliminateBtn.disabled) return; + + const hintsLeftRaw = + typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3; + + // No hints left: show red error feedback with existing animation and exit + if (hintsLeftRaw <= 0) { + eliminateBtn.classList.add('btn-error', 'shake'); + setTimeout(() => eliminateBtn.classList.remove('shake', 'btn-error'), 460); + return; + } + + const allListItems = Array.from(answersListElement.querySelectorAll('li')); + + // Always use "eliminate two wrong answers" + hint(currentQuestion, allListItems, 0); + + // Mark hint on this question + currentQuestion.usedHint = true; + + // Consume one global hint + quizData.hintsLeft = Math.max(0, hintsLeftRaw - 1); + try { + saveState(); + } catch {} + + // Update the on-screen tracker "used/3" + const trackerEl = document.getElementById('hint-tracker'); + if (trackerEl) { + const usedAfter = 3 - (quizData.hintsLeft || 0); + const usedSpan = trackerEl.querySelector('.hint-used'); + if (usedSpan) usedSpan.textContent = `${usedAfter}/3`; + } + + // Also refresh progress dots immediately + try { + updateProgressMarks(); + } catch {} + + // Lock hint for this question + hintUsed = true; + eliminateBtn.classList.add('hint-used'); + refreshEliminateUI(); }); + } - document - .getElementById(NEXT_QUESTION_BUTTON_ID) - .addEventListener('click', nextQuestion); const nextBtn = document.getElementById(NEXT_QUESTION_BUTTON_ID); nextBtn.addEventListener('click', () => { const current = quizData.questions[quizData.currentQuestionIndex]; @@ -123,38 +548,113 @@ export const initQuestionPage = () => { nextQuestion(); }); - document - .getElementById(AVOID_QUESTION_BUTTON_ID) - .addEventListener('click', avoidQuestion); - - document - .getElementById(RESTART_QUIZ) - .addEventListener('click', resetQuizState); + const avoidBtn = document.getElementById(AVOID_QUESTION_BUTTON_ID); + if (avoidBtn) { + avoidBtn.addEventListener('click', avoidQuestion); + } }; const nextQuestion = () => { quizData.currentQuestionIndex += 1; + try { + saveState(); + } catch {} initQuestionPage(); }; const avoidQuestion = () => { - // go to the next question - quizData.currentQuestionIndex = quizData.currentQuestionIndex + 1; - console.log('Question avoided'); + const listEl = document.getElementById(ANSWERS_LIST_ID); + const current = quizData.questions[quizData.currentQuestionIndex]; - initQuestionPage(); // display the new question -}; + if (listEl && current) { + const items = Array.from(listEl.querySelectorAll('li')); + // disable clicks + items.forEach((li) => (li.style.pointerEvents = 'none')); + + // mark correct answer briefly + items.forEach((li) => { + if (li.dataset.key === current.correct) { + li.style.backgroundColor = 'green'; + li.setAttribute('data-badge', '✓'); + } + }); + + const avoid = document.getElementById(AVOID_QUESTION_BUTTON_ID); + if (avoid) avoid.disabled = true; + const eliminate = document.getElementById(ELIMINATE_TWO_ANSWERS_BUTTON_ID); + if (eliminate) eliminate.disabled = true; -const hint = (currentQuestion, allListItems) => { - const wrongItems = allListItems.filter((li) => { - return li.dataset.key !== currentQuestion.correct; - }); //get all the wrong options - const elements = new Set(); + // mark this question as avoided and update progress immediately + current.avoided = true; + try { + saveState(); + } catch {} + try { + updateProgressMarks(); + } catch {} - while (elements.size < 2) { - const randomIndex = Math.floor(Math.random() * wrongItems.length); - elements.add(wrongItems[randomIndex]); + setTimeout(() => { + quizData.currentQuestionIndex = quizData.currentQuestionIndex + 1; + console.log('Question avoided'); + try { + saveState(); + } catch {} + initQuestionPage(); + }, 800); + } else { + // fallback immediate + current.avoided = true; + try { + saveState(); + } catch {} + try { + updateProgressMarks(); + } catch {} + quizData.currentQuestionIndex = quizData.currentQuestionIndex + 1; + console.log('Question avoided'); + initQuestionPage(); } +}; - elements.forEach((ele) => (ele.hidden = true)); +const hint = (currentQuestion, allListItems, hintTypeIndex) => { + if (currentQuestion) currentQuestion.usedHint = true; + if (hintTypeIndex === 0) { + // Eliminate 2 wrong answers + const wrongItems = allListItems.filter((li) => { + return li.dataset.key !== currentQuestion.correct; + }); + const elements = new Set(); + + while (elements.size < 2) { + const randomIndex = Math.floor(Math.random() * wrongItems.length); + elements.add(wrongItems[randomIndex]); + } + + elements.forEach((ele) => { + ele.classList.add('eliminate-out'); + ele.setAttribute('aria-hidden', 'true'); + ele.style.pointerEvents = 'none'; + ele.addEventListener( + 'animationend', + () => { + ele.hidden = true; + }, + { once: true } + ); + }); + } else if (hintTypeIndex === 1) { + // Show Link + if (currentQuestion.links && currentQuestion.links.length > 0) { + const link = currentQuestion.links[0]; + alert(`Helpful link: ${link.text}\n${link.href}`); + } else { + alert('No helpful link available for this question.'); + } + } else if (hintTypeIndex === 2) { + // First Letter + const correctKey = currentQuestion.correct; + const correctAnswer = currentQuestion.answers[correctKey]; + const firstLetter = correctAnswer.charAt(0).toUpperCase(); + alert(`The correct answer starts with: ${firstLetter}`); + } }; diff --git a/src/pages/welcomePage.js b/src/pages/welcomePage.js index c9297fb..7cb44d7 100644 --- a/src/pages/welcomePage.js +++ b/src/pages/welcomePage.js @@ -1,4 +1,8 @@ -import { USER_INTERFACE_ID, START_QUIZ_BUTTON_ID } from '../constants.js'; +import { + USER_INTERFACE_ID, + START_QUIZ_BUTTON_ID, + STORAGE_KEY, +} from '../constants.js'; import { createWelcomeElement } from '../views/welcomeView.js'; import { initQuestionPage } from './questionPage.js'; import { quizData } from '../data.js'; @@ -11,13 +15,88 @@ import { quizData } from '../data.js'; export const initWelcomePage = () => { const userInterface = document.getElementById(USER_INTERFACE_ID); userInterface.innerHTML = ''; + userInterface.classList.add('welcome-mode'); const welcomeElement = createWelcomeElement(); userInterface.appendChild(welcomeElement); + // Re-attach hero card tilt/entrance (moved from view to page to keep views pure) + const card = welcomeElement.querySelector('.card'); + if (card) { + const reduce = + window.matchMedia && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + // Smooth entrance + requestAnimationFrame(() => card.classList.add('enter')); + + if (!reduce) { + const maxTilt = 6; // degrees + const onMove = (ev) => { + const rect = card.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + const dx = (ev.clientX - cx) / (rect.width / 2); + const dy = (ev.clientY - cy) / (rect.height / 2); + // map -1..1 to degrees + const rx = (dx * maxTilt).toFixed(2) + 'deg'; // rotateY + const ry = (-dy * maxTilt).toFixed(2) + 'deg'; // rotateX + card.style.setProperty('--rx', rx); + card.style.setProperty('--ry', ry); + + // for radial highlight overlay + const mx = ((ev.clientX - rect.left) / rect.width) * 100; + const my = ((ev.clientY - rect.top) / rect.height) * 100; + card.style.setProperty('--mx', mx.toFixed(2) + '%'); + card.style.setProperty('--my', my.toFixed(2) + '%'); + }; + + const onLeave = () => { + card.style.setProperty('--rx', '0deg'); + card.style.setProperty('--ry', '0deg'); + card.style.setProperty('--mx', '50%'); + card.style.setProperty('--my', '0%'); + }; + + card.addEventListener('mousemove', onMove); + card.addEventListener('mouseleave', onLeave); + } + } + document .getElementById(START_QUIZ_BUTTON_ID) .addEventListener('click', startQuiz); + + // Rules modal interactions + const rulesBtn = document.getElementById('rules-button'); + if (rulesBtn) { + rulesBtn.addEventListener('click', () => { + const modal = document.getElementById('rules-modal'); + if (modal) modal.classList.add('show'); + }); + } + + const closeBtn = document.getElementById('close-rules'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + const modal = document.getElementById('rules-modal'); + if (modal) modal.classList.remove('show'); + }); + } + + const modal = document.getElementById('rules-modal'); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) modal.classList.remove('show'); + }); + + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('show')) { + modal.classList.remove('show'); + } + }); + } }; /** @@ -29,5 +108,22 @@ export const initWelcomePage = () => { const startQuiz = () => { const nameInput = document.getElementById('user-name-input'); quizData.userName = nameInput.value || 'Mysterious Stranger'; // fallback + + // Persist initial state (username + current index + selections) + try { + const payload = { + userName: quizData.userName || '', + currentQuestionIndex: quizData.currentQuestionIndex, + hintsLeft: + typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3, + selectedMap: Object.fromEntries( + quizData.questions.map((q) => [q.id, q.selected ?? null]) + ), + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); + } catch { + // ignore storage errors + } + initQuestionPage(); }; diff --git a/src/views/questionView.js b/src/views/questionView.js index 9219b19..6f483f8 100644 --- a/src/views/questionView.js +++ b/src/views/questionView.js @@ -1,41 +1,67 @@ -import { ANSWERS_LIST_ID } from '../constants.js'; import { + ANSWERS_LIST_ID, NEXT_QUESTION_BUTTON_ID, AVOID_QUESTION_BUTTON_ID, - ELEMINATE_TWO_ANSWERS_BUTTON_ID, - RESTART_QUIZ, + ELIMINATE_TWO_ANSWERS_BUTTON_ID, + SCORE_INDICATOR_ID, + PROGRESS_BAR_ID, + PROGRESS_FILL_ID, + PROGRESS_MARKS_ID, + SALAD_BOWL_ID, + PRIZE_POP_ID, } from '../constants.js'; import { createPage } from '../utils/createPage.js'; -/** - * Create a full question element - * @returns {Element} - */ -export const createQuestionElement = (question) => { +export const createQuestionElement = (question, scoreText) => { + // Gets current score and total from text + const match = /(\d+)\s*\/\s*(\d+)/.exec(String(scoreText || '')); + const current = match ? Number(match[1]) : 0; // Current score number + const total = match ? Number(match[2]) : 0; // Total questions number + + // Builds the page with question, score, progress, and buttons return createPage( '', String.raw` + +
    +

    ${question}

    +
    + Score +
    + ${current} + / + ${total} +
    +
    +
    + Hints + 0/3 +
    +
    - -

    ${question}

    + +
    +
    + +
    - -
      -
    + +
    - + +
    - + +
      - + +
      + + + +
      ` ); - - // return element; }; diff --git a/src/views/welcomeView.js b/src/views/welcomeView.js index 9a56549..5c79c47 100644 --- a/src/views/welcomeView.js +++ b/src/views/welcomeView.js @@ -1,5 +1,5 @@ -import { START_QUIZ_BUTTON_ID } from '../constants.js'; import { createPage } from '../utils/createPage.js'; +import { START_QUIZ_BUTTON_ID } from '../constants.js'; /** * Create the welcome screen @@ -14,9 +14,13 @@ export const createWelcomeElement = () => {
      @@ -37,54 +41,31 @@ export const createWelcomeElement = () => { + + + +
      + + + ` ); - // Parallax entrance and tilt effects for the welcome card - const card = element.querySelector('.card'); - if (card) { - const reduce = - window.matchMedia && - window.matchMedia('(prefers-reduced-motion: reduce)').matches; - - // Smooth entrance - requestAnimationFrame(() => card.classList.add('enter')); - - if (!reduce) { - const maxTilt = 6; // degrees - const onMove = (ev) => { - const rect = card.getBoundingClientRect(); - const cx = rect.left + rect.width / 2; - const cy = rect.top + rect.height / 2; - const dx = (ev.clientX - cx) / (rect.width / 2); - const dy = (ev.clientY - cy) / (rect.height / 2); - // map -1..1 to degrees - const rx = (dx * maxTilt).toFixed(2) + 'deg'; // rotateY - const ry = (-dy * maxTilt).toFixed(2) + 'deg'; // rotateX - card.style.setProperty('--rx', rx); - card.style.setProperty('--ry', ry); - - // for radial highlight overlay - const mx = ((ev.clientX - rect.left) / rect.width) * 100; - const my = ((ev.clientY - rect.top) / rect.height) * 100; - card.style.setProperty('--mx', mx.toFixed(2) + '%'); - card.style.setProperty('--my', my.toFixed(2) + '%'); - }; - - const onLeave = () => { - card.style.setProperty('--rx', '0deg'); - card.style.setProperty('--ry', '0deg'); - card.style.setProperty('--mx', '50%'); - card.style.setProperty('--my', '0%'); - }; - - card.addEventListener('mousemove', onMove); - card.addEventListener('mouseleave', onLeave); - } - } + // interactions moved to page: see initWelcomePage() return element; }; From fb9c472d2cbeafd1bc4f4f46440382ea488b7138 Mon Sep 17 00:00:00 2001 From: Majd Hamde Date: Sat, 27 Sep 2025 05:06:14 +0200 Subject: [PATCH 4/4] fix with Prettier I HATE PRETTIER --- index.html | 3 - public/style.css | 219 ++++++++++++++++++++++++++------------ src/app.js | 38 ++----- src/pages/endPage.js | 53 +++++++++ src/pages/questionPage.js | 55 ++++++---- src/views/questionView.js | 33 +++--- 6 files changed, 263 insertions(+), 138 deletions(-) diff --git a/index.html b/index.html index b3fdc18..6777ba1 100644 --- a/index.html +++ b/index.html @@ -12,10 +12,7 @@ -
      - - diff --git a/public/style.css b/public/style.css index ada478d..562746f 100644 --- a/public/style.css +++ b/public/style.css @@ -839,6 +839,15 @@ body.question-surface #user-interface button { color: #fff !important; /* keep title and buttons white on question pages */ text-shadow: none; } +/* Force score widget and hint tracker to black for clarity */ +body.question-surface #user-interface .score-widget, +body.question-surface #user-interface .score-widget * { + color: #000 !important; +} +body.question-surface #user-interface .hint-tracker, +body.question-surface #user-interface .hint-tracker * { + color: #000 !important; +} /* Subtle accent border/glow on question card hover */ body.question-surface #user-interface.centered:hover { @@ -1296,12 +1305,12 @@ body.question-surface.question-light #user-interface button { .modal { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.65); display: none; align-items: center; justify-content: center; z-index: 1000; - backdrop-filter: blur(4px); + backdrop-filter: blur(8px); } .modal.show { @@ -1310,21 +1319,37 @@ body.question-surface.question-light #user-interface button { .modal-content { position: relative; - max-width: 400px; - width: 90%; - max-height: 70vh; + max-width: 520px; + width: min(92vw, 520px); + max-height: 76vh; overflow-y: auto; - padding: 24px 24px 80px; /* Extra bottom padding to ensure close button visibility */ + padding: 24px 24px 20px; background: linear-gradient( 180deg, - rgba(255, 255, 255, 0.1), - rgba(255, 255, 255, 0.06) + rgba(255, 255, 255, 0.98), + rgba(250, 253, 251, 0.96) ); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 16px; - box-shadow: var(--glass-shadow); - color: #1a1a1a; - text-align: center; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 20px; + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.15); + color: #111; + text-align: left; + backdrop-filter: none; + -webkit-backdrop-filter: none; + animation: fadeInUp 300ms cubic-bezier(0.2, 0.75, 0.25, 1); +} +.modal-content::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 6px; + border-top-left-radius: 20px; + border-top-right-radius: 20px; + background: linear-gradient(90deg, #2e7d32, #66bb6a); + box-shadow: 0 2px 8px rgba(102, 187, 106, 0.35); + pointer-events: none; } .modal-close { @@ -1358,22 +1383,73 @@ body.question-surface.question-light #user-interface button { margin: 0 0 16px; font-size: 1.4rem; font-weight: 600; - color: #1a1a1a; + color: #0d3b1e; + text-align: center; +} +.modal h2::after { + content: ''; + display: block; + width: 56px; + height: 4px; + margin: 8px auto 0; + border-radius: 999px; + background: linear-gradient(90deg, #2e7d32, #66bb6a); + box-shadow: 0 2px 8px rgba(102, 187, 106, 0.4); } .rules-list { - list-style: disc; - padding: 0 0 0 20px; - margin: 0; + list-style: none; + padding: 0; + margin: 16px 0; text-align: left; + display: grid; + gap: 12px; +} +.rules-list li + li { + border-top: none; } .rules-list li { - padding: 12px 0; - color: #333; + position: relative; + padding: 16px 20px 16px 56px; + color: #ffffff; font-size: 16px; - line-height: 1.5; - font-weight: 500; + line-height: 1.6; + font-weight: 700; + background: linear-gradient(135deg, var(--green-600), var(--green-500)); + border: none; + border-radius: 14px; + box-shadow: 0 10px 26px rgba(27, 94, 32, 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 0 0 0 rgba(102, 187, 106, 0); + transition: transform 160ms ease, box-shadow 220ms ease, background 200ms ease, + filter 180ms ease; +} + +.rules-list li:hover { + background: linear-gradient(135deg, #2e7d32, #1b5e20); + transform: translateY(-2px) scale(1.02); + box-shadow: 0 18px 36px rgba(27, 94, 32, 0.55), + 0 0 0 6px rgba(102, 187, 106, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.22); + filter: brightness(1.03); +} + +.rules-list li::before { + content: '✓'; + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + background: #ffffff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 800; + color: #2e7d32; + box-shadow: 0 3px 8px rgba(76, 175, 80, 0.35); } @media (max-width: 520px) { @@ -1413,6 +1489,11 @@ body.question-surface.question-light #user-interface button { font-weight: 700; color: var(--accent-info); } +/* Make hint button flash red when hints are 0 (class is added by JS) */ +#eliminate-two-answers-button.btn-error { + --btn-bg: linear-gradient(135deg, #f44336, #e53935) !important; + color: #fff !important; +} /* ===== Quiz Header Layout (structured, responsive) ===== */ .quiz-header { @@ -1575,81 +1656,87 @@ button.hint-used::before { /* Progress marks: per-question "balls" positioned along the bar */ .progress-marks { - position: absolute; - top: -14px; /* sit neatly above the bar */ + position: relative; + top: 0; left: 0; right: 0; - height: 16px; + height: 40px; pointer-events: none; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px; + margin: 12px 0 16px; } .progress-marks .ball { - position: absolute; - top: 0; - transform: translateX(-50%); - width: 14px; - height: 14px; + position: relative; + width: 30px; + height: 30px; border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.55); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); - transition: transform 220ms ease, filter 220ms ease, opacity 220ms ease; - opacity: 0.95; + border: 4px solid rgba(255, 255, 255, 0.85); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.22); + transition: transform 280ms ease, filter 280ms ease, opacity 280ms ease, + box-shadow 280ms ease; + opacity: 0.98; + backdrop-filter: blur(2px); } .progress-marks .ball:hover { - transform: translateX(-50%) scale(1.12); + transform: scale(1.12); filter: saturate(1.08) brightness(1.04); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } /* Ball state colors */ .progress-marks .ball.state-unanswered { - background: #9e9e9e; - opacity: 0.7; - border-color: rgba(255, 255, 255, 0.4); + background: linear-gradient(180deg, #bdbdbd, #9e9e9e); + opacity: 0.6; + border-color: rgba(255, 255, 255, 0.5); } .progress-marks .ball.state-correct { - background: linear-gradient(180deg, #43a047, #66bb6a); + background: linear-gradient(180deg, #4caf50, #43a047); + border-color: rgba(76, 175, 80, 0.8); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } .progress-marks .ball.state-wrong { - background: linear-gradient(180deg, #e53935, #f44336); + background: linear-gradient(180deg, #f44336, #e53935); + border-color: rgba(244, 67, 54, 0.8); + box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } .progress-marks .ball.state-hinted-only { - background: linear-gradient(180deg, #1976d2, #2196f3); + background: linear-gradient(180deg, #2196f3, #1976d2); + border-color: rgba(33, 150, 243, 0.8); + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } .progress-marks .ball.state-hinted-correct { - background: linear-gradient(90deg, #2196f3 0 50%, #43a047 50% 100%); + background: linear-gradient(90deg, #2196f3 0% 50%, #4caf50 50% 100%); + border-color: rgba(76, 175, 80, 0.8); + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } .progress-marks .ball.state-hinted-wrong { - background: linear-gradient(90deg, #2196f3 0 50%, #e53935 50% 100%); + background: linear-gradient(90deg, #2196f3 0% 50%, #f44336 50% 100%); + border-color: rgba(244, 67, 54, 0.8); + box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } .progress-marks .ball.state-avoided { - background: linear-gradient(180deg, #ff8c00, #ffa62b); + background: linear-gradient(180deg, #ff9800, #f57c00); + border-color: rgba(255, 152, 0, 0.8); + box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3); } -/* Progress bar itself */ +/* Progress bar removed - only using balls now */ .progress-bar { - position: relative; - width: 100%; - height: 12px; - border-radius: 999px; - overflow: hidden; - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.22); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); + display: none; } .progress-fill { - height: 100%; - width: 0%; - border-radius: 999px; - /* Rotating accent gradient per question with safe fallbacks */ - background: linear-gradient( - 90deg, - var(--progress-c1, var(--accent-lemon)), - var(--progress-c2, var(--accent-carrot)), - var(--progress-c3, var(--accent-fresh-green)), - var(--progress-c4, var(--accent-tomato)) - ); - /* Progress motion ~360ms */ - transition: width 360ms cubic-bezier(0.2, 0.75, 0.25, 1); - box-shadow: 0 0 10px rgba(107, 203, 119, 0.5); + display: none; } /* ===== Prize System Enhancements (progress, prize pop, salad bowl, confetti) ===== */ diff --git a/src/app.js b/src/app.js index 13b2f97..817a0b0 100644 --- a/src/app.js +++ b/src/app.js @@ -87,7 +87,8 @@ export function saveState() { const payload = { userName: quizData.userName || '', currentQuestionIndex: quizData.currentQuestionIndex, - hintsLeft: typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3, + hintsLeft: + typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3, selectedMap: Object.fromEntries( quizData.questions.map((q) => [q.id, q.selected ?? null]) ), @@ -439,44 +440,17 @@ export function resetQuestionTheme() { } // Generate a favicon from an emoji and set it as the page icon -export function setEmojiFavicon(emoji) { +export function setEmojiFavicon(_) { try { - const canvas = document.createElement('canvas'); - const size = 64; - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - - // transparent background - ctx.clearRect(0, 0, size, size); - // draw emoji centered - ctx.font = - '48px "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",system-ui,sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(emoji, size / 2, size / 2); - - const url = canvas.toDataURL('image/png'); - - let link = document.querySelector('link[rel="icon"]'); - if (!link) { - link = document.createElement('link'); - link.rel = 'icon'; - document.head.appendChild(link); - } - link.href = url; - } catch (e) { - // Fallback: SVG data URL with emoji glyph - const svg = `${emoji}`; - const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); let link = document.querySelector('link[rel="icon"]'); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } - link.href = url; - } + // Always use the provided static favicon file + link.href = './public/favicon.ico'; + } catch {} } // Run app on page load diff --git a/src/pages/endPage.js b/src/pages/endPage.js index 71ef563..e8faf08 100644 --- a/src/pages/endPage.js +++ b/src/pages/endPage.js @@ -4,6 +4,48 @@ import { changeBackground, resetQuizState, clearState } from '../app.js'; import { USER_INTERFACE_ID, PRIZE_STEPS } from '../constants.js'; import { quizData } from '../data.js'; +// Select a result GIF based on final score (use classic thresholds; fallback to ratio) +function getResultGif(score, total) { + if (typeof score !== 'number' || typeof total !== 'number' || total <= 0) { + return null; + } + // Explicit mapping when total is 10 (classic flow) + if (total === 10) { + if (score >= 10) + return { file: 'champ.gif', alt: 'Champion', text: 'Champion! 🏆' }; + if (score >= 9) + return { file: 'fighter.gif', alt: 'Fighter', text: 'Fighter! ⭐️' }; + if (score >= 6) + return { + file: 'halfchamp.gif', + alt: 'Half Champion', + text: 'Half Champion! 💪', + }; + if (score >= 3) + return { + file: 'halfloser.gif', + alt: 'Half Loser', + text: 'Keep Going! 🌱', + }; + return { file: 'loser.gif', alt: 'Try Again', text: 'Try Again! 🥲' }; + } + // Ratio-based fallback for other totals + const r = score / total; + if (r >= 1) + return { file: 'champ.gif', alt: 'Champion', text: 'Champion! 🏆' }; + if (r >= 0.9) + return { file: 'fighter.gif', alt: 'Fighter', text: 'Fighter! ⭐️' }; + if (r >= 0.6) + return { + file: 'halfchamp.gif', + alt: 'Half Champion', + text: 'Half Champion! 💪', + }; + if (r >= 0.3) + return { file: 'halfloser.gif', alt: 'Half Loser', text: 'Keep Going! 🌱' }; + return { file: 'loser.gif', alt: 'Try Again', text: 'Try Again! 🥲' }; +} + export const showEndPage = () => { changeBackground(999); const userInterface = document.getElementById(USER_INTERFACE_ID); @@ -30,6 +72,16 @@ export const showEndPage = () => { `; + const gif = getResultGif(score, total); + const gifHtml = gif + ? ` +
      + ${gif.alt} +
      ${gif.text}
      +
      + ` + : ''; + const endElement = createPage( 'end-page', ` @@ -38,6 +90,7 @@ export const showEndPage = () => {
      Score: ${score} / ${total}

      ${subline}

      ${prizeHtml} + ${gifHtml} ` ); diff --git a/src/pages/questionPage.js b/src/pages/questionPage.js index b5e8555..ae95970 100644 --- a/src/pages/questionPage.js +++ b/src/pages/questionPage.js @@ -63,13 +63,15 @@ function countWrong() { // Updates the progress bar to show how many questions done function updateProgressBar() { const fill = document.getElementById(PROGRESS_FILL_ID); // Gets the progress fill element - if (!fill) return; // Stops if no element - const total = quizData.questions.length || 0; // Total questions - const answered = countAnswered(); // How many answered - const pct = total ? Math.round((answered / total) * 100) : 0; // Percentage done - fill.style.width = pct + '%'; // Sets bar width - fill.setAttribute('aria-valuenow', String(pct)); // Accessibility update - // Updates prize marks too + // If a classic progress bar exists, update it. Otherwise, still update dots. + if (fill) { + const total = quizData.questions.length || 0; // Total questions + const answered = countAnswered(); // How many answered + const pct = total ? Math.round((answered / total) * 100) : 0; // Percentage done + fill.style.width = pct + '%'; // Sets bar width + fill.setAttribute('aria-valuenow', String(pct)); // Accessibility update + } + // Always update progress dots, even when bar is hidden/removed try { updateProgressMarks(); } catch {} @@ -99,8 +101,7 @@ function buildProgressMarks() { for (let i = 0; i < total; i++) { const dot = document.createElement('span'); dot.className = 'ball'; - const left = total <= 1 ? 0 : (i / (total - 1)) * 100; - dot.style.left = left + '%'; + // Positioning is handled by flex layout in CSS; no left offset needed dot.dataset.index = String(i); cont.appendChild(dot); } @@ -320,7 +321,7 @@ const storeAnswer = (questionIndex, selectedOption) => { saveState(); } catch {} updateScoreIndicator(prevScore); // Updates score - updateProgressBar(); // Updates progress + updateProgressBar(); // Updates dots (and legacy bar if present) updateSaladBowl(); // Updates bowl showPrizePop(wasCorrect); // Shows prize or fail }; @@ -356,7 +357,7 @@ export const initQuestionPage = () => { // Adds floating items and updates displays ensureFloatingIngredients(); // Floating salad buildProgressMarks(); // Progress marks - updateProgressBar(); // Progress bar + updateProgressBar(); // Progress dots updateSaladBowl(); // Salad bowl // Updates score display @@ -466,7 +467,7 @@ export const initQuestionPage = () => { nextBtnEl.classList.remove('btn-error', 'shake'); } updateScoreIndicator(); // Updates score - updateProgressBar(); // Updates progress + updateProgressBar(); // Updates progress dots updateSaladBowl(); // Updates bowl for consistency } @@ -475,8 +476,9 @@ export const initQuestionPage = () => { let hintUsed = false; const refreshEliminateUI = () => { - const hintsLeft = typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3; - eliminateBtn.textContent = 'Hint'; + const hintsLeft = + typeof quizData.hintsLeft === 'number' ? quizData.hintsLeft : 3; + eliminateBtn.textContent = `Hint (${hintsLeft} left)`; const shouldDisable = hintUsed || !!currentQuestion.selected; eliminateBtn.disabled = shouldDisable; if (hintUsed) { @@ -498,11 +500,16 @@ export const initQuestionPage = () => { // No hints left: show red error feedback with existing animation and exit if (hintsLeftRaw <= 0) { eliminateBtn.classList.add('btn-error', 'shake'); - setTimeout(() => eliminateBtn.classList.remove('shake', 'btn-error'), 460); + setTimeout( + () => eliminateBtn.classList.remove('shake', 'btn-error'), + 460 + ); return; } - const allListItems = Array.from(answersListElement.querySelectorAll('li')); + const allListItems = Array.from( + answersListElement.querySelectorAll('li') + ); // Always use "eliminate two wrong answers" hint(currentQuestion, allListItems, 0); @@ -669,13 +676,17 @@ const hint = (currentQuestion, allListItems, hintTypeIndex) => { //reset button behavior const resetQuiz = () => { - quizData.scoreCorrect = 0; - quizData.scoreIncorrect = 0; - quizData.currentQuestionIndex = 0; - console.log('Quiz reset'); - initWelcomePage(); // back to welcome page + // Clear any persisted state and reset in-memory quiz data + try { + clearState(); + } catch {} + resetQuizState(); + + // Return to welcome screen with default background + changeBackground(-1); + initWelcomePage(); - //RESET background and question theme + // RESET background and question theme styling hooks (safeguard) requestAnimationFrame(() => { resetQuestionTheme(); }); diff --git a/src/views/questionView.js b/src/views/questionView.js index 5ef7761..12fac17 100644 --- a/src/views/questionView.js +++ b/src/views/questionView.js @@ -4,8 +4,6 @@ import { AVOID_QUESTION_BUTTON_ID, ELIMINATE_TWO_ANSWERS_BUTTON_ID, SCORE_INDICATOR_ID, - PROGRESS_BAR_ID, - PROGRESS_FILL_ID, PROGRESS_MARKS_ID, SALAD_BOWL_ID, PRIZE_POP_ID, @@ -13,11 +11,21 @@ import { } from '../constants.js'; import { createPage } from '../utils/createPage.js'; +// Escape HTML entities to safely render user-provided text +const escapeHTML = (str) => + String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + export const createQuestionElement = (question, scoreText) => { // Gets current score and total from text const match = /(\d+)\s*\/\s*(\d+)/.exec(String(scoreText || '')); const current = match ? Number(match[1]) : 0; // Current score number const total = match ? Number(match[2]) : 0; // Total questions number + const title = escapeHTML(String(question ?? '')); // Safe title text // Builds the page with question, score, progress, and buttons return createPage( @@ -25,7 +33,7 @@ export const createQuestionElement = (question, scoreText) => { String.raw`
      -

      ${question}

      +

      ${title}

      Score
      @@ -34,19 +42,14 @@ export const createQuestionElement = (question, scoreText) => { ${total}
      -
      +
      Hints 0/3
      - -
      -
      - -
      + +
      @@ -59,10 +62,10 @@ export const createQuestionElement = (question, scoreText) => {
      - - - - + + + +
      ` );