diff --git a/src/pages/CapitalsQuiz.svelte b/src/pages/CapitalsQuiz.svelte index d3e3d10..a4b9001 100644 --- a/src/pages/CapitalsQuiz.svelte +++ b/src/pages/CapitalsQuiz.svelte @@ -9,14 +9,15 @@ } from "../quizLogic/quizGlobalStats.js"; import { saveSessionState, - loadSessionState, clearSessionState, createNewSessionState, + restoreSessionState, } from "../quizLogic/quizSession.js"; import { createAdvanceTimer } from "../quizLogic/advanceTimer.js"; import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js"; import { quizInfo } from "../quizInfo/CapitalsQuizInfo.js"; - import { onMount } from "svelte"; + import { onMount, onDestroy } from "svelte"; + import { loadFlags as loadFlagsShared, getCountryName, getFlagImage, pickWeightedFlag } from "../quizLogic/flags.js"; import Header from "../components/Header.svelte"; import Footer from "../components/Footer.svelte"; import Achievements from "../components/Achievements.svelte"; @@ -36,10 +37,7 @@ let quizSubpage = "welcome"; // 'welcome' or 'quiz' let selectedAnswer = null; let answered = false; - let isAnswered = false; - let resultMessage = ""; let showResult = false; - let timeoutId = null; let showCountryInfo = false; let showResultCountryInfo = false; @@ -56,8 +54,8 @@ // Scoring let score = { correct: 0, total: 0, skipped: 0 }; let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 }; - let wrongAnswers = new Map(); // Track flags answered incorrectly: flag.name -> count - let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count + let wrongAnswers = new Map(); + let correctAnswers = new Map(); // Achievement System let currentStreak = 0; @@ -90,7 +88,7 @@ let showSessionResults = false; let sessionInProgress = false; let sessionStartTime = null; - let sessionRestoredFromReload = false; // Track if session was restored from page reload + let sessionRestoredFromReload = false; // Update achievement count when achievements component is available $: if (achievementsComponent) { @@ -188,76 +186,39 @@ await loadFlags(); settingsLoaded = true; - // Load or initialize session - const loadedSession = loadSessionState("capitalsQuizSessionState", null); - if (loadedSession) { - // Restore session - sessionInProgress = loadedSession.sessionInProgress; - currentSessionQuestions = loadedSession.currentSessionQuestions || 0; - sessionStats = loadedSession.sessionStats || { - correct: 0, - wrong: 0, - skipped: 0, - total: 0, - sessionLength, - }; - score = loadedSession.score || { correct: 0, total: 0, skipped: 0 }; - currentQuestion = loadedSession.currentQuestion; - selectedAnswer = loadedSession.selectedAnswer; - showResult = loadedSession.showResult || false; - gameState = loadedSession.gameState || "question"; - quizSubpage = "quiz"; - sessionStartTime = loadedSession.sessionStartTime; - questionKey = loadedSession.questionKey || 0; + // Load or initialize session (centralized) + const restored = restoreSessionState("capitalsQuizSessionState"); + if (restored && restored.sessionInProgress) { + sessionInProgress = restored.sessionInProgress; + currentSessionQuestions = restored.currentSessionQuestions; + sessionStats = restored.sessionStats; + score = restored.score; + currentQuestion = restored.currentQuestion; + selectedAnswer = restored.selectedAnswer; + showResult = restored.showResult; + gameState = restored.gameState; + quizSubpage = restored.quizSubpage; + sessionStartTime = restored.sessionStartTime; + questionKey = restored.questionKey || 0; + sessionRestoredFromReload = restored.sessionRestoredFromReload; - // Mark that session was restored from reload - sessionRestoredFromReload = true; - - // If we don't have a current question, generate one if (!currentQuestion) { generateQuestion(); } } else { - // No saved state, show welcome page quizSubpage = "welcome"; gameState = "welcome"; } }); + // Cleanup on component destroy: cancel any running advance timer + onDestroy(() => { + if (advanceTimer) advanceTimer.cancel(); + }); + async function loadFlags() { - try { - const response = await fetch("/data/flags.json"); - const data = await response.json(); - // Filter for only country flags (has "Country" tag) and ensure we have country name and capital - flags = data.filter( - (flag) => - !flag.disable && - flag.meta?.country && - flag.meta?.capital && - flag.tags && - flag.tags.includes("Country"), - ); - - // Remove duplicates based on country name - const uniqueFlags = []; - const seenCountries = new Set(); - - for (const flag of flags) { - const countryName = flag.meta.country.toLowerCase().trim(); - if (!seenCountries.has(countryName)) { - seenCountries.add(countryName); - uniqueFlags.push(flag); - } - } - - flags = uniqueFlags; - console.log( - `Loaded ${flags.length} unique country flags for capitals quiz`, - ); - } catch (error) { - console.error("Error loading flags:", error); - flags = []; - } + flags = await loadFlagsShared({ requireCapital: true }); + console.log(`Loaded ${flags.length} unique country flags for capitals quiz`); } function generateQuestion() { @@ -297,48 +258,9 @@ }, 0); } - // Pick correct answer with adaptive learning settings - let correctFlag; - - // Simple fallback to avoid uninitialized variable errors - if (settingsLoaded && (focusWrongAnswers || reduceCorrectAnswers)) { - // Re-enable adaptive learning - // Create weighted array based on learning settings - const weightedFlags = []; - for (const flag of flags) { - const wrongCount = wrongAnswers.get(flag.name) || 0; - const correctCount = correctAnswers.get(flag.name) || 0; - - let weight = 1; // Base weight - - // Increase weight for flags with wrong answers (if setting enabled) - if (focusWrongAnswers && wrongCount > 0) { - weight = Math.min(wrongCount + 1, 4); // Max 4x weight for wrong answers - } - - // Decrease weight for flags with correct answers (if setting enabled) - if (reduceCorrectAnswers && correctCount > 0) { - weight = weight / Math.min(correctCount + 1, 4); // Reduce weight, min 0.25x - } - - // Add flag to weighted array based on calculated weight - const finalWeight = Math.max(0.25, weight); // Minimum weight to ensure variety - const timesToAdd = Math.ceil(finalWeight); - for (let i = 0; i < timesToAdd; i++) { - weightedFlags.push(flag); - } - } - - if (weightedFlags.length > 0) { - correctFlag = - weightedFlags[Math.floor(Math.random() * weightedFlags.length)]; - } else { - correctFlag = flags[Math.floor(Math.random() * flags.length)]; - } - } else { - // Normal random selection - correctFlag = flags[Math.floor(Math.random() * flags.length)]; - } + // Pick correct answer using shared helper (handles adaptive weighting) + const pick = pickWeightedFlag(flags, { focusWrongAnswers, reduceCorrectAnswers }, wrongAnswers, correctAnswers); + const correctFlag = pick || flags[Math.floor(Math.random() * flags.length)]; const correctCapital = correctFlag.meta.capital.toLowerCase(); @@ -687,18 +609,11 @@ } } - function getCountryName(flag) { - return flag.meta?.country || flag.name || "Unknown"; - } - + // Use shared getCountryName/getFlagImage helpers from flags.js function getCapitalName(flag) { return flag.meta?.capital || "Unknown"; } - function getFlagImage(flag) { - return `/images/flags/${flag.path}`; - } - function handleAchievementsUnlocked() { achievementCount = updateAchievementCount(achievementsComponent); } diff --git a/src/pages/FlagQuiz.svelte b/src/pages/FlagQuiz.svelte index f711608..550c143 100644 --- a/src/pages/FlagQuiz.svelte +++ b/src/pages/FlagQuiz.svelte @@ -9,14 +9,15 @@ import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js"; import { saveSessionState, - loadSessionState, clearSessionState, createNewSessionState, } from "../quizLogic/quizSession.js"; + import { restoreSessionState } from "../quizLogic/quizSession.js"; import { createAdvanceTimer } from "../quizLogic/advanceTimer.js"; import { quizInfo } from "../quizInfo/FlagQuizInfo.js"; - import { onMount } from "svelte"; + import { onMount, onDestroy } from "svelte"; + import { loadFlags as loadFlagsShared, getCountryName, getFlagImage, pickWeightedFlag } from "../quizLogic/flags.js"; import Header from "../components/Header.svelte"; import Footer from "../components/Footer.svelte"; import InlineSvg from "../components/InlineSvg.svelte"; @@ -31,8 +32,6 @@ let questionType = "flag-to-country"; // 'flag-to-country' or 'country-to-flag' // Question and answer arrays - let currentCountryOptions = []; - let currentFlagOptions = []; let correctAnswer = ""; // Game states @@ -40,10 +39,7 @@ let quizSubpage = "welcome"; // 'welcome' or 'quiz' let selectedAnswer = null; let answered = false; - let isAnswered = false; - let resultMessage = ""; let showResult = false; - let timeoutId = null; let showCountryInfo = false; let showResultCountryInfo = false; @@ -58,8 +54,8 @@ // Scoring let score = { correct: 0, total: 0, skipped: 0 }; let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 }; - let wrongAnswers = new Map(); // Track flags answered incorrectly: flag.name -> count - let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count + let wrongAnswers = new Map(); + let correctAnswers = new Map(); // Achievement System let currentStreak = 0; @@ -89,7 +85,7 @@ let showSessionResults = false; let sessionInProgress = false; let sessionStartTime = null; - let sessionRestoredFromReload = false; // Track if session was restored from page reload + let sessionRestoredFromReload = false; // Update achievement count when achievements component is available $: if (achievementsComponent) { @@ -193,36 +189,24 @@ await loadFlags(); settingsLoaded = true; - // Load or initialize session - const loaded = loadSessionState("flagQuizSessionState", null); - if (loaded) { - if (loaded.sessionInProgress) { - sessionInProgress = loaded.sessionInProgress; - currentSessionQuestions = loaded.currentSessionQuestions || 0; - sessionStats = loaded.sessionStats || { - correct: 0, - wrong: 0, - skipped: 0, - total: 0, - sessionLength, - }; - score = loaded.score || { correct: 0, total: 0, skipped: 0 }; - currentQuestion = loaded.currentQuestion; - selectedAnswer = loaded.selectedAnswer; - showResult = loaded.showResult || false; - gameState = loaded.gameState || "question"; - quizSubpage = "quiz"; - sessionStartTime = loaded.sessionStartTime; - questionKey = loaded.questionKey || 0; + // Load or initialize session (centralized) + const restored = restoreSessionState("flagQuizSessionState"); + if (restored && restored.sessionInProgress) { + sessionInProgress = restored.sessionInProgress; + currentSessionQuestions = restored.currentSessionQuestions; + sessionStats = restored.sessionStats; + score = restored.score; + currentQuestion = restored.currentQuestion; + selectedAnswer = restored.selectedAnswer; + showResult = restored.showResult; + gameState = restored.gameState; + quizSubpage = restored.quizSubpage; + sessionStartTime = restored.sessionStartTime; + questionKey = restored.questionKey || 0; + sessionRestoredFromReload = restored.sessionRestoredFromReload; - sessionRestoredFromReload = true; - - if (!currentQuestion) { - generateQuestion(); - } - } else { - quizSubpage = "welcome"; - gameState = "welcome"; + if (!currentQuestion) { + generateQuestion(); } } else { quizSubpage = "welcome"; @@ -230,39 +214,16 @@ } }); + // Cleanup on component destroy: cancel any running advance timer + onDestroy(() => { + if (advanceTimer) advanceTimer.cancel(); + }); + // use shared applyTheme from ../utils/theme.js async function loadFlags() { - try { - const response = await fetch("/data/flags.json"); - const data = await response.json(); - // Filter for only country flags (has "Country" tag) and ensure we have country name - flags = data.filter( - (flag) => - !flag.disable && - flag.meta?.country && - flag.tags && - flag.tags.includes("Country"), - ); - - // Remove duplicates based on country name - const uniqueFlags = []; - const seenCountries = new Set(); - - for (const flag of flags) { - const countryName = flag.meta.country.toLowerCase().trim(); - if (!seenCountries.has(countryName)) { - seenCountries.add(countryName); - uniqueFlags.push(flag); - } - } - - flags = uniqueFlags; - console.log(`Loaded ${flags.length} unique country flags for quiz`); - } catch (error) { - console.error("Error loading flags:", error); - flags = []; - } + flags = await loadFlagsShared(); + console.log(`Loaded ${flags.length} unique country flags for quiz`); } function generateQuestion() { @@ -305,50 +266,11 @@ // Randomly choose question type questionType = Math.random() < 0.5 ? "flag-to-country" : "country-to-flag"; - // Pick correct answer with adaptive learning settings - let correctFlag; + // Pick correct answer with shared helper (handles adaptive settings) + const pick = pickWeightedFlag(flags, { focusWrongAnswers, reduceCorrectAnswers }, wrongAnswers, correctAnswers); + const correctFlag = pick || flags[Math.floor(Math.random() * flags.length)]; - // Simple fallback to avoid uninitialized variable errors - if (settingsLoaded && (focusWrongAnswers || reduceCorrectAnswers)) { - // Re-enable adaptive learning - // Create weighted array based on learning settings - const weightedFlags = []; - for (const flag of flags) { - const wrongCount = wrongAnswers.get(flag.name) || 0; - const correctCount = correctAnswers.get(flag.name) || 0; - - let weight = 1; // Base weight - - // Increase weight for flags with wrong answers (if setting enabled) - if (focusWrongAnswers && wrongCount > 0) { - weight = Math.min(wrongCount + 1, 4); // Max 4x weight for wrong answers - } - - // Decrease weight for flags with correct answers (if setting enabled) - if (reduceCorrectAnswers && correctCount > 0) { - weight = weight / Math.min(correctCount + 1, 4); // Reduce weight, min 0.25x - } - - // Add flag to weighted array based on calculated weight - const finalWeight = Math.max(0.25, weight); // Minimum weight to ensure variety - const timesToAdd = Math.ceil(finalWeight); - for (let i = 0; i < timesToAdd; i++) { - weightedFlags.push(flag); - } - } - - if (weightedFlags.length > 0) { - correctFlag = - weightedFlags[Math.floor(Math.random() * weightedFlags.length)]; - } else { - correctFlag = flags[Math.floor(Math.random() * flags.length)]; - } - } else { - // Normal random selection - correctFlag = flags[Math.floor(Math.random() * flags.length)]; - } - - const correctCountry = getCountryName(correctFlag).toLowerCase(); + const correctCountry = getCountryName(correctFlag).toLowerCase(); // Generate 3 wrong answers ensuring no duplicate country names const wrongOptions = []; @@ -356,7 +278,7 @@ while (wrongOptions.length < 3 && wrongOptions.length < flags.length - 1) { const randomFlag = flags[Math.floor(Math.random() * flags.length)]; - const randomCountry = getCountryName(randomFlag).toLowerCase(); + const randomCountry = getCountryName(randomFlag).toLowerCase(); if (!usedCountries.has(randomCountry)) { wrongOptions.push(randomFlag); @@ -373,9 +295,7 @@ } // Combine correct and wrong answers - const allOptions = [correctFlag, ...wrongOptions].sort( - () => Math.random() - 0.5, - ); + const allOptions = [correctFlag, ...wrongOptions].sort(() => Math.random() - 0.5); currentQuestion = { type: questionType, correct: correctFlag, @@ -683,13 +603,7 @@ } } - function getCountryName(flag) { - return flag.meta?.country || flag.name || "Unknown"; - } - - function getFlagImage(flag) { - return `/images/flags/${flag.path}`; - } + // use shared getCountryName from ../quizLogic/flags.js function handleAchievementsUnlocked() { achievementCount = updateAchievementCount(achievementsComponent); diff --git a/src/quizLogic/flags.js b/src/quizLogic/flags.js new file mode 100644 index 0000000..521d654 --- /dev/null +++ b/src/quizLogic/flags.js @@ -0,0 +1,73 @@ +// Utilities for loading and choosing flags used by quiz pages +export async function loadFlags({ requireCapital = false } = {}) { + try { + const response = await fetch('/data/flags.json'); + const data = await response.json(); + + let flags = data.filter((flag) => { + if (flag.disable) return false; + if (!flag.meta?.country) return false; + if (requireCapital && !flag.meta?.capital) return false; + if (!flag.tags || !flag.tags.includes('Country')) return false; + return true; + }); + + // Remove duplicates based on country name + const uniqueFlags = []; + const seenCountries = new Set(); + + for (const flag of flags) { + const countryName = (flag.meta.country || flag.name || '').toLowerCase().trim(); + if (!seenCountries.has(countryName)) { + seenCountries.add(countryName); + uniqueFlags.push(flag); + } + } + + return uniqueFlags; + } catch (err) { + console.error('flags.loadFlags error', err); + return []; + } +} + +export function getCountryName(flag) { + return flag?.meta?.country || flag?.name || 'Unknown'; +} + +export function getFlagImage(flag) { + return `/images/flags/${flag.path}`; +} + +// Pick a flag from a weighted list based on wrong/correct answer maps and settings +export function pickWeightedFlag(flags, { focusWrongAnswers = false, reduceCorrectAnswers = false } = {}, wrongAnswers = new Map(), correctAnswers = new Map()) { + if (!Array.isArray(flags) || flags.length === 0) return null; + + // If no adaptive settings enabled, return a random flag + if (!focusWrongAnswers && !reduceCorrectAnswers) { + return flags[Math.floor(Math.random() * flags.length)]; + } + + const weighted = []; + for (const flag of flags) { + const wrongCount = wrongAnswers.get(flag.name) || 0; + const correctCount = correctAnswers.get(flag.name) || 0; + + let weight = 1; + if (focusWrongAnswers && wrongCount > 0) { + weight = Math.min(wrongCount + 1, 4); + } + if (reduceCorrectAnswers && correctCount > 0) { + weight = weight / Math.min(correctCount + 1, 4); + } + + const finalWeight = Math.max(0.25, weight); + const times = Math.ceil(finalWeight); + for (let i = 0; i < times; i++) weighted.push(flag); + } + + if (weighted.length > 0) { + return weighted[Math.floor(Math.random() * weighted.length)]; + } + return flags[Math.floor(Math.random() * flags.length)]; +} diff --git a/src/quizLogic/quizSession.js b/src/quizLogic/quizSession.js index 9b984f2..cfe9ec6 100644 --- a/src/quizLogic/quizSession.js +++ b/src/quizLogic/quizSession.js @@ -40,3 +40,32 @@ export function createNewSessionState(sessionLength = 10) { sessionRestoredFromReload: false, }; } + +// Restore session helper: loads saved session state (if any) and returns a normalized object +export function restoreSessionState(key) { + const loaded = loadSessionState(key, null); + if (!loaded) return null; + + const session = { + sessionInProgress: loaded.sessionInProgress || false, + currentSessionQuestions: loaded.currentSessionQuestions || 0, + sessionStats: loaded.sessionStats || { + correct: 0, + wrong: 0, + skipped: 0, + total: 0, + sessionLength: loaded?.sessionStats?.sessionLength || 10, + }, + score: loaded.score || { correct: 0, total: 0, skipped: 0 }, + currentQuestion: loaded.currentQuestion || null, + selectedAnswer: loaded.selectedAnswer || null, + showResult: loaded.showResult || false, + gameState: loaded.gameState || "question", + quizSubpage: loaded.quizSubpage || "quiz", + sessionStartTime: loaded.sessionStartTime || null, + questionKey: loaded.questionKey || 0, + sessionRestoredFromReload: true, + }; + + return session; +} diff --git a/src/utils/theme.js b/src/utils/theme.js index 1cbd1ea..d3823dd 100644 --- a/src/utils/theme.js +++ b/src/utils/theme.js @@ -1,3 +1,5 @@ +import { writable } from 'svelte/store'; + export function applyTheme(theme) { let effectiveTheme = theme; if (theme === "system") { @@ -14,6 +16,12 @@ export function setTheme(newTheme) { if (newTheme === "light" || newTheme === "dark" || newTheme === "system") { // Persist choice and apply immediately localStorage.setItem("theme", newTheme); + // Update reactive store so $themeStore updates everywhere immediately + try { + themeStore.set(newTheme); + } catch (e) { + // If themeStore isn't initialized yet, ignore — it will be set on module init + } console.log("[Theme] setTheme:", newTheme); setTimeout(() => applyTheme(newTheme), 0); return newTheme; @@ -25,5 +33,4 @@ export function getStoredTheme(defaultTheme = "system") { } // Svelte store for reactive theme across components -import { writable } from 'svelte/store'; export const themeStore = writable(getStoredTheme());