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";
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);
}

View File

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

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,
};
}
// 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) {
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());