Refactor session management in quizzes to use centralized restoreSessionState function and improve flag loading logic with shared utilities

This commit is contained in:
sHa
2025-08-15 12:15:55 +03:00
parent 44078938fe
commit d61366b91d
5 changed files with 179 additions and 241 deletions

View File

@@ -9,14 +9,15 @@
} from "../quizLogic/quizGlobalStats.js"; } from "../quizLogic/quizGlobalStats.js";
import { import {
saveSessionState, saveSessionState,
loadSessionState,
clearSessionState, clearSessionState,
createNewSessionState, createNewSessionState,
restoreSessionState,
} from "../quizLogic/quizSession.js"; } from "../quizLogic/quizSession.js";
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js"; import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js"; import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
import { quizInfo } from "../quizInfo/CapitalsQuizInfo.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 Header from "../components/Header.svelte";
import Footer from "../components/Footer.svelte"; import Footer from "../components/Footer.svelte";
import Achievements from "../components/Achievements.svelte"; import Achievements from "../components/Achievements.svelte";
@@ -36,10 +37,7 @@
let quizSubpage = "welcome"; // 'welcome' or 'quiz' let quizSubpage = "welcome"; // 'welcome' or 'quiz'
let selectedAnswer = null; let selectedAnswer = null;
let answered = false; let answered = false;
let isAnswered = false;
let resultMessage = "";
let showResult = false; let showResult = false;
let timeoutId = null;
let showCountryInfo = false; let showCountryInfo = false;
let showResultCountryInfo = false; let showResultCountryInfo = false;
@@ -56,8 +54,8 @@
// Scoring // Scoring
let score = { correct: 0, total: 0, skipped: 0 }; let score = { correct: 0, total: 0, skipped: 0 };
let gameStats = { correct: 0, wrong: 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 wrongAnswers = new Map();
let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count let correctAnswers = new Map();
// Achievement System // Achievement System
let currentStreak = 0; let currentStreak = 0;
@@ -90,7 +88,7 @@
let showSessionResults = false; let showSessionResults = false;
let sessionInProgress = false; let sessionInProgress = false;
let sessionStartTime = null; 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 // Update achievement count when achievements component is available
$: if (achievementsComponent) { $: if (achievementsComponent) {
@@ -188,76 +186,39 @@
await loadFlags(); await loadFlags();
settingsLoaded = true; settingsLoaded = true;
// Load or initialize session // Load or initialize session (centralized)
const loadedSession = loadSessionState("capitalsQuizSessionState", null); const restored = restoreSessionState("capitalsQuizSessionState");
if (loadedSession) { if (restored && restored.sessionInProgress) {
// Restore session sessionInProgress = restored.sessionInProgress;
sessionInProgress = loadedSession.sessionInProgress; currentSessionQuestions = restored.currentSessionQuestions;
currentSessionQuestions = loadedSession.currentSessionQuestions || 0; sessionStats = restored.sessionStats;
sessionStats = loadedSession.sessionStats || { score = restored.score;
correct: 0, currentQuestion = restored.currentQuestion;
wrong: 0, selectedAnswer = restored.selectedAnswer;
skipped: 0, showResult = restored.showResult;
total: 0, gameState = restored.gameState;
sessionLength, quizSubpage = restored.quizSubpage;
}; sessionStartTime = restored.sessionStartTime;
score = loadedSession.score || { correct: 0, total: 0, skipped: 0 }; questionKey = restored.questionKey || 0;
currentQuestion = loadedSession.currentQuestion; sessionRestoredFromReload = restored.sessionRestoredFromReload;
selectedAnswer = loadedSession.selectedAnswer;
showResult = loadedSession.showResult || false;
gameState = loadedSession.gameState || "question";
quizSubpage = "quiz";
sessionStartTime = loadedSession.sessionStartTime;
questionKey = loadedSession.questionKey || 0;
// Mark that session was restored from reload
sessionRestoredFromReload = true;
// If we don't have a current question, generate one
if (!currentQuestion) { if (!currentQuestion) {
generateQuestion(); generateQuestion();
} }
} else { } else {
// No saved state, show welcome page
quizSubpage = "welcome"; quizSubpage = "welcome";
gameState = "welcome"; gameState = "welcome";
} }
}); });
// Cleanup on component destroy: cancel any running advance timer
onDestroy(() => {
if (advanceTimer) advanceTimer.cancel();
});
async function loadFlags() { async function loadFlags() {
try { flags = await loadFlagsShared({ requireCapital: true });
const response = await fetch("/data/flags.json"); console.log(`Loaded ${flags.length} unique country flags for capitals quiz`);
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 = [];
}
} }
function generateQuestion() { function generateQuestion() {
@@ -297,48 +258,9 @@
}, 0); }, 0);
} }
// Pick correct answer with adaptive learning settings // Pick correct answer using shared helper (handles adaptive weighting)
let correctFlag; 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 correctCapital = correctFlag.meta.capital.toLowerCase(); const correctCapital = correctFlag.meta.capital.toLowerCase();
@@ -687,18 +609,11 @@
} }
} }
function getCountryName(flag) { // Use shared getCountryName/getFlagImage helpers from flags.js
return flag.meta?.country || flag.name || "Unknown";
}
function getCapitalName(flag) { function getCapitalName(flag) {
return flag.meta?.capital || "Unknown"; return flag.meta?.capital || "Unknown";
} }
function getFlagImage(flag) {
return `/images/flags/${flag.path}`;
}
function handleAchievementsUnlocked() { function handleAchievementsUnlocked() {
achievementCount = updateAchievementCount(achievementsComponent); achievementCount = updateAchievementCount(achievementsComponent);
} }

View File

@@ -9,14 +9,15 @@
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js"; import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
import { import {
saveSessionState, saveSessionState,
loadSessionState,
clearSessionState, clearSessionState,
createNewSessionState, createNewSessionState,
} from "../quizLogic/quizSession.js"; } from "../quizLogic/quizSession.js";
import { restoreSessionState } from "../quizLogic/quizSession.js";
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js"; import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
import { quizInfo } from "../quizInfo/FlagQuizInfo.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 Header from "../components/Header.svelte";
import Footer from "../components/Footer.svelte"; import Footer from "../components/Footer.svelte";
import InlineSvg from "../components/InlineSvg.svelte"; import InlineSvg from "../components/InlineSvg.svelte";
@@ -31,8 +32,6 @@
let questionType = "flag-to-country"; // 'flag-to-country' or 'country-to-flag' let questionType = "flag-to-country"; // 'flag-to-country' or 'country-to-flag'
// Question and answer arrays // Question and answer arrays
let currentCountryOptions = [];
let currentFlagOptions = [];
let correctAnswer = ""; let correctAnswer = "";
// Game states // Game states
@@ -40,10 +39,7 @@
let quizSubpage = "welcome"; // 'welcome' or 'quiz' let quizSubpage = "welcome"; // 'welcome' or 'quiz'
let selectedAnswer = null; let selectedAnswer = null;
let answered = false; let answered = false;
let isAnswered = false;
let resultMessage = "";
let showResult = false; let showResult = false;
let timeoutId = null;
let showCountryInfo = false; let showCountryInfo = false;
let showResultCountryInfo = false; let showResultCountryInfo = false;
@@ -58,8 +54,8 @@
// Scoring // Scoring
let score = { correct: 0, total: 0, skipped: 0 }; let score = { correct: 0, total: 0, skipped: 0 };
let gameStats = { correct: 0, wrong: 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 wrongAnswers = new Map();
let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count let correctAnswers = new Map();
// Achievement System // Achievement System
let currentStreak = 0; let currentStreak = 0;
@@ -89,7 +85,7 @@
let showSessionResults = false; let showSessionResults = false;
let sessionInProgress = false; let sessionInProgress = false;
let sessionStartTime = null; 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 // Update achievement count when achievements component is available
$: if (achievementsComponent) { $: if (achievementsComponent) {
@@ -193,36 +189,24 @@
await loadFlags(); await loadFlags();
settingsLoaded = true; settingsLoaded = true;
// Load or initialize session // Load or initialize session (centralized)
const loaded = loadSessionState("flagQuizSessionState", null); const restored = restoreSessionState("flagQuizSessionState");
if (loaded) { if (restored && restored.sessionInProgress) {
if (loaded.sessionInProgress) { sessionInProgress = restored.sessionInProgress;
sessionInProgress = loaded.sessionInProgress; currentSessionQuestions = restored.currentSessionQuestions;
currentSessionQuestions = loaded.currentSessionQuestions || 0; sessionStats = restored.sessionStats;
sessionStats = loaded.sessionStats || { score = restored.score;
correct: 0, currentQuestion = restored.currentQuestion;
wrong: 0, selectedAnswer = restored.selectedAnswer;
skipped: 0, showResult = restored.showResult;
total: 0, gameState = restored.gameState;
sessionLength, quizSubpage = restored.quizSubpage;
}; sessionStartTime = restored.sessionStartTime;
score = loaded.score || { correct: 0, total: 0, skipped: 0 }; questionKey = restored.questionKey || 0;
currentQuestion = loaded.currentQuestion; sessionRestoredFromReload = restored.sessionRestoredFromReload;
selectedAnswer = loaded.selectedAnswer;
showResult = loaded.showResult || false;
gameState = loaded.gameState || "question";
quizSubpage = "quiz";
sessionStartTime = loaded.sessionStartTime;
questionKey = loaded.questionKey || 0;
sessionRestoredFromReload = true; if (!currentQuestion) {
generateQuestion();
if (!currentQuestion) {
generateQuestion();
}
} else {
quizSubpage = "welcome";
gameState = "welcome";
} }
} else { } else {
quizSubpage = "welcome"; 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 // use shared applyTheme from ../utils/theme.js
async function loadFlags() { async function loadFlags() {
try { flags = await loadFlagsShared();
const response = await fetch("/data/flags.json"); console.log(`Loaded ${flags.length} unique country flags for quiz`);
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 = [];
}
} }
function generateQuestion() { function generateQuestion() {
@@ -305,50 +266,11 @@
// Randomly choose question type // Randomly choose question type
questionType = Math.random() < 0.5 ? "flag-to-country" : "country-to-flag"; questionType = Math.random() < 0.5 ? "flag-to-country" : "country-to-flag";
// Pick correct answer with adaptive learning settings // Pick correct answer with shared helper (handles adaptive settings)
let correctFlag; 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 const correctCountry = getCountryName(correctFlag).toLowerCase();
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();
// Generate 3 wrong answers ensuring no duplicate country names // Generate 3 wrong answers ensuring no duplicate country names
const wrongOptions = []; const wrongOptions = [];
@@ -356,7 +278,7 @@
while (wrongOptions.length < 3 && wrongOptions.length < flags.length - 1) { while (wrongOptions.length < 3 && wrongOptions.length < flags.length - 1) {
const randomFlag = flags[Math.floor(Math.random() * flags.length)]; const randomFlag = flags[Math.floor(Math.random() * flags.length)];
const randomCountry = getCountryName(randomFlag).toLowerCase(); const randomCountry = getCountryName(randomFlag).toLowerCase();
if (!usedCountries.has(randomCountry)) { if (!usedCountries.has(randomCountry)) {
wrongOptions.push(randomFlag); wrongOptions.push(randomFlag);
@@ -373,9 +295,7 @@
} }
// Combine correct and wrong answers // Combine correct and wrong answers
const allOptions = [correctFlag, ...wrongOptions].sort( const allOptions = [correctFlag, ...wrongOptions].sort(() => Math.random() - 0.5);
() => Math.random() - 0.5,
);
currentQuestion = { currentQuestion = {
type: questionType, type: questionType,
correct: correctFlag, correct: correctFlag,
@@ -683,13 +603,7 @@
} }
} }
function getCountryName(flag) { // use shared getCountryName from ../quizLogic/flags.js
return flag.meta?.country || flag.name || "Unknown";
}
function getFlagImage(flag) {
return `/images/flags/${flag.path}`;
}
function handleAchievementsUnlocked() { function handleAchievementsUnlocked() {
achievementCount = updateAchievementCount(achievementsComponent); achievementCount = updateAchievementCount(achievementsComponent);

73
src/quizLogic/flags.js Normal file
View File

@@ -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)];
}

View File

@@ -40,3 +40,32 @@ export function createNewSessionState(sessionLength = 10) {
sessionRestoredFromReload: false, 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;
}

View File

@@ -1,3 +1,5 @@
import { writable } from 'svelte/store';
export function applyTheme(theme) { export function applyTheme(theme) {
let effectiveTheme = theme; let effectiveTheme = theme;
if (theme === "system") { if (theme === "system") {
@@ -14,6 +16,12 @@ export function setTheme(newTheme) {
if (newTheme === "light" || newTheme === "dark" || newTheme === "system") { if (newTheme === "light" || newTheme === "dark" || newTheme === "system") {
// Persist choice and apply immediately // Persist choice and apply immediately
localStorage.setItem("theme", newTheme); 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); console.log("[Theme] setTheme:", newTheme);
setTimeout(() => applyTheme(newTheme), 0); setTimeout(() => applyTheme(newTheme), 0);
return newTheme; return newTheme;
@@ -25,5 +33,4 @@ export function getStoredTheme(defaultTheme = "system") {
} }
// Svelte store for reactive theme across components // Svelte store for reactive theme across components
import { writable } from 'svelte/store';
export const themeStore = writable(getStoredTheme()); export const themeStore = writable(getStoredTheme());