Refactor quiz components to improve session management and theme handling

- Updated QuizSettings component to handle full reset of quiz data and reload the application after clearing local storage.
- Refactored CapitalsQuiz and FlagQuiz components to utilize shared session management functions, reducing code duplication.
- Introduced createNewSessionState function to streamline session initialization across quizzes.
- Implemented a shared advance timer utility for managing auto-advance functionality in quizzes.
- Centralized theme management into a utility module, allowing for consistent theme application and storage across components.
- Removed redundant session state management code from quiz components, enhancing maintainability.
This commit is contained in:
sHa
2025-08-15 02:03:58 +03:00
parent 63d6c99ff1
commit 44078938fe
8 changed files with 432 additions and 630 deletions

View File

@@ -1,6 +1,7 @@
<script>
import { onMount } from "svelte";
import { collections } from "./collections.js";
import { applyTheme, setTheme, themeStore } from "./utils/theme.js";
import Router from "svelte-spa-router/Router.svelte";
import Home from "./pages/Home.svelte";
import Preview from "./pages/Preview.svelte";
@@ -26,7 +27,9 @@
let logos = [];
let filteredLogos = [];
let displayLogos = [];
let theme = "system";
let theme;
// Keep local theme in sync with shared theme store
$: theme = $themeStore;
let allTags = [];
let selectedTags = [];
let selectedBrands = [];
@@ -178,7 +181,7 @@
logos: [],
filteredLogos: [],
displayLogos: [],
theme,
theme: $themeStore,
effectiveTheme: "light",
viewMode,
searchQuery,
@@ -430,7 +433,7 @@
setGridView,
setListView,
setCompactView,
setTheme: (newTheme) => {
setTheme: (newTheme) => {
console.log("window.appData.setTheme called with:", newTheme);
setTheme(newTheme);
},
@@ -543,30 +546,6 @@
document.body.removeChild(link);
}
function applyTheme() {
let effectiveTheme = theme;
if (theme === "system") {
effectiveTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
// Apply theme both ways for compatibility
document.documentElement.setAttribute("data-theme", effectiveTheme);
document.documentElement.className = effectiveTheme; // Add class-based theming
console.log("[Theme] Applied theme:", effectiveTheme);
}
function setTheme(newTheme) {
if (newTheme === "light" || newTheme === "dark" || newTheme === "system") {
console.log("App.svelte: Setting theme to:", newTheme);
theme = newTheme;
localStorage.setItem("theme", newTheme);
console.log("[Theme] setTheme:", newTheme);
// Apply theme immediately after setting
setTimeout(() => applyTheme(), 0);
}
}
function toggleTag(tag) {
console.log("App: Toggling tag:", tag);
if (selectedTags.includes(tag)) {
@@ -837,11 +816,11 @@
<Router {routes} let:Component>
<svelte:component
this={Component}
{displayLogos}
allLogos={logos}
{theme}
{setTheme}
this={Component}
{displayLogos}
allLogos={logos}
{theme}
setTheme={setTheme}
{viewMode}
{setGridView}
{setListView}

View File

@@ -29,7 +29,6 @@
}
function handleOverlayClick(event) {
// Only close if clicking on the overlay itself, not the modal content
if (event.target === event.currentTarget) {
toggleSettings();
}
@@ -41,8 +40,45 @@
}
function handleResetStats() {
dispatch('resetStats');
// Perform a full reset here so individual quizzes don't need to implement reset handlers
const keysToRemove = [
// Flag quiz
'flagQuizStats',
'flagQuizWrongAnswers',
'flagQuizCorrectAnswers',
'flagQuizAchievements',
'flagQuizSessionState',
// Capitals quiz
'capitalsQuizStats',
'capitalsQuizWrongAnswers',
'capitalsQuizCorrectAnswers',
'capitalsQuizAchievements',
'capitalsQuizSessionState',
// Shared/global
'globalQuizStats'
];
try {
for (const k of keysToRemove) {
localStorage.removeItem(k);
}
} catch (e) {
console.error('Error clearing quiz data during reset:', e);
}
// Close confirmation modal and settings overlay, notify parent
showResetConfirmation = false;
if (showSettings) {
showSettings = false;
dispatch('settingsToggle', false);
}
// Give a moment for UI to close, then reload so components pick up cleared state
setTimeout(() => {
if (typeof window !== 'undefined') {
window.location.reload();
}
}, 150);
}
// Reactive statements to dispatch settings changes

View File

@@ -1,14 +1,24 @@
<script>
import { updateAchievementCount as sharedUpdateAchievementCount } from '../quizLogic/quizAchievements.js';
import { saveSettings as sharedSaveSettings, loadSettings as sharedLoadSettings } from '../quizLogic/quizSettings.js';
import { loadGlobalStats as sharedLoadGlobalStats, updateGlobalStats as sharedUpdateGlobalStats } from '../quizLogic/quizGlobalStats.js';
import { saveSessionState as sharedSaveSessionState, loadSessionState as sharedLoadSessionState, clearSessionState as sharedClearSessionState } from '../quizLogic/quizSession.js';
import { playCorrectSound as sharedPlayCorrectSound, playWrongSound as sharedPlayWrongSound } from '../quizLogic/quizSound.js';
import { quizInfo } from '../quizInfo/CapitalsQuizInfo.js';
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
import { updateAchievementCount } from "../quizLogic/quizAchievements.js";
import { saveSettings, loadSettings } from "../quizLogic/quizSettings.js";
import {
loadGlobalStats,
updateGlobalStats,
} from "../quizLogic/quizGlobalStats.js";
import {
saveSessionState,
loadSessionState,
clearSessionState,
createNewSessionState,
} 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 Header from "../components/Header.svelte";
import Footer from "../components/Footer.svelte";
import InlineSvg from "../components/InlineSvg.svelte";
import Achievements from "../components/Achievements.svelte";
import QuizSettings from "../components/QuizSettings.svelte";
import QuizInfo from "../components/QuizInfo.svelte";
@@ -34,10 +44,11 @@
let showResultCountryInfo = false;
// Auto-advance timer variables
let autoAdvanceTimer = null;
// advance timer (shared)
let advanceTimer;
let timerProgress = 0;
let timerDuration = 2000; // 2 seconds
let timerStartTime = 0;
// Force component re-render key to prevent button state persistence
let questionKey = 0;
@@ -81,23 +92,14 @@
let sessionStartTime = null;
let sessionRestoredFromReload = false; // Track if session was restored from page reload
// Theme
let theme = "system";
function setTheme(t) {
localStorage.setItem("theme", t);
applyTheme(t);
theme = t;
}
// Update achievement count when achievements component is available
$: if (achievementsComponent) {
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
achievementCount = updateAchievementCount(achievementsComponent);
}
// Save settings when they change (after initial load)
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
sharedSaveSettings("capitalsQuizSettings", {
saveSettings("capitalsQuizSettings", {
autoAdvance,
focusWrongAnswers,
reduceCorrectAnswers,
@@ -108,9 +110,7 @@
// Load game stats from localStorage
onMount(async () => {
// Initialize theme
theme = localStorage.getItem("theme") || "system";
applyTheme(theme);
applyTheme($themeStore);
// Set window.appData for header compatibility
if (typeof window !== "undefined") {
@@ -118,8 +118,8 @@
...window.appData,
collection: "capitals",
setCollection: () => {},
theme,
setTheme,
theme: $themeStore,
setTheme: setTheme,
};
// Load saved game stats
@@ -140,7 +140,9 @@
}
// Load wrong answers tracking
const savedWrongAnswers = localStorage.getItem("capitalsQuizWrongAnswers");
const savedWrongAnswers = localStorage.getItem(
"capitalsQuizWrongAnswers",
);
if (savedWrongAnswers) {
try {
const loadedWrongAnswers = JSON.parse(savedWrongAnswers);
@@ -164,7 +166,7 @@
}
// Load settings
const loadedSettings = sharedLoadSettings("capitalsQuizSettings", {
const loadedSettings = loadSettings("capitalsQuizSettings", {
autoAdvance,
focusWrongAnswers,
reduceCorrectAnswers,
@@ -180,80 +182,14 @@
}
// Load global stats and update them
sharedLoadGlobalStats("globalQuizStats");
loadGlobalStats("globalQuizStats");
}
await loadFlags();
settingsLoaded = true;
// Load or initialize session
loadSessionState();
});
function applyTheme(theme) {
let effectiveTheme = theme;
if (theme === "system") {
effectiveTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
document.documentElement.setAttribute("data-theme", effectiveTheme);
document.documentElement.className = effectiveTheme;
}
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 = [];
}
}
function saveSessionState() {
const sessionState = {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
};
sharedSaveSessionState("capitalsQuizSessionState", sessionState);
}
function loadSessionState() {
const loadedSession = sharedLoadSessionState("capitalsQuizSessionState", null);
const loadedSession = loadSessionState("capitalsQuizSessionState", null);
if (loadedSession) {
// Restore session
sessionInProgress = loadedSession.sessionInProgress;
@@ -286,10 +222,42 @@
quizSubpage = "welcome";
gameState = "welcome";
}
}
});
function clearSessionState() {
sharedClearSessionState("capitalsQuizSessionState");
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 = [];
}
}
function generateQuestion() {
@@ -410,7 +378,19 @@
console.log("Generated capitals question:", currentQuestion);
// Save session state
saveSessionState();
saveSessionState("capitalsQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
}
function selectAnswer(index) {
@@ -436,7 +416,7 @@
capitalsCorrect++;
// Play correct sound
playCorrectSound();
playCorrectSound(soundEnabled);
// Track correct answer for this flag
if (currentQuestion.correct?.name) {
@@ -476,7 +456,7 @@
currentStreak = 0; // Reset streak on wrong answer
// Play wrong sound
playWrongSound();
playWrongSound(soundEnabled);
// Track wrong answer for this flag
if (currentQuestion.correct?.name) {
@@ -499,7 +479,7 @@
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
// Update global stats
updateGlobalStats(isCorrect);
updateGlobalStats("globalQuizStats", "capitalsQuiz", isCorrect);
// Check for new achievements
if (achievementsComponent) {
@@ -507,7 +487,19 @@
}
// Save session state
saveSessionState();
saveSessionState("capitalsQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
// Check if session is complete
if (currentSessionQuestions >= sessionLength) {
@@ -559,10 +551,22 @@
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
// Update global stats (skipped question)
updateGlobalStats(null, true);
updateGlobalStats("globalQuizStats", "capitalsQuiz", null, true);
// Save session state
saveSessionState();
saveSessionState("capitalsQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
// Check if session is complete
if (currentSessionQuestions >= sessionLength) {
@@ -578,69 +582,49 @@
function startAutoAdvanceTimer(duration) {
timerDuration = duration;
timerProgress = 0;
timerStartTime = Date.now();
// Clear any existing timer
if (autoAdvanceTimer) {
clearInterval(autoAdvanceTimer);
if (!advanceTimer) {
advanceTimer = createAdvanceTimer(
(p) => (timerProgress = p),
() => generateQuestion(),
);
}
// Update progress every 50ms for smooth animation
autoAdvanceTimer = setInterval(() => {
const elapsed = Date.now() - timerStartTime;
timerProgress = Math.min((elapsed / duration) * 100, 100);
if (timerProgress >= 100) {
clearInterval(autoAdvanceTimer);
autoAdvanceTimer = null;
timerProgress = 0;
generateQuestion();
}
}, 50);
advanceTimer.start(duration);
}
function cancelAutoAdvanceTimer() {
if (autoAdvanceTimer) {
clearInterval(autoAdvanceTimer);
autoAdvanceTimer = null;
timerProgress = 0;
}
if (advanceTimer) advanceTimer.cancel();
timerProgress = 0;
}
function startNewSession() {
// Reset session data
score = { correct: 0, total: 0, skipped: 0 };
currentSessionQuestions = 0;
sessionStats = {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength,
};
sessionInProgress = true;
sessionStartTime = Date.now();
showSessionResults = false;
sessionRestoredFromReload = false; // Reset reload flag for new sessions
// Create canonical new session state and apply it
const s = createNewSessionState(sessionLength);
score = s.score;
currentSessionQuestions = s.currentSessionQuestions;
sessionStats = s.sessionStats;
sessionInProgress = s.sessionInProgress;
sessionStartTime = s.sessionStartTime;
showSessionResults = s.showSessionResults;
sessionRestoredFromReload = s.sessionRestoredFromReload;
// Switch to quiz subpage
quizSubpage = "quiz";
gameState = "loading";
// Generate first question
generateQuestion();
}
function endSession() {
// Track perfect rounds for achievements
if (sessionStats.correct === sessionLength && sessionStats.wrong === 0 && sessionStats.skipped === 0) {
if (
sessionStats.correct === sessionLength &&
sessionStats.wrong === 0 &&
sessionStats.skipped === 0
) {
perfectRounds++;
}
// Clear session state
sessionInProgress = false;
clearSessionState();
clearSessionState("capitalsQuizSessionState");
// Switch to welcome/stats page
quizSubpage = "welcome";
@@ -648,20 +632,6 @@
showSessionResults = true; // Show results on welcome page
}
function resetGame() {
endSession();
}
function resetStats() {
gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
}
function toggleSettings() {
showSettings = !showSettings;
}
function handleSettingsChange(event) {
const {
autoAdvance: newAutoAdvance,
@@ -688,53 +658,6 @@
showResetConfirmation = event.detail;
}
function handleResetStats() {
// Reset game statistics
gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
score = { correct: 0, total: 0, skipped: 0 };
currentStreak = 0;
currentSessionQuestions = 0;
sessionStats = {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength,
};
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
// Reset wrong answers tracking
wrongAnswers = new Map();
localStorage.removeItem("capitalsQuizWrongAnswers");
// Reset correct answers tracking
correctAnswers = new Map();
localStorage.removeItem("capitalsQuizCorrectAnswers");
// Reset achievements if component is available
if (achievementsComponent) {
achievementsComponent.resetConsecutiveSkips();
}
showResetConfirmation = false;
}
function handleSessionPlayAgain() {
resetGame();
}
function handleSessionGoToGames() {
window.location.hash = "#/game";
}
function handleSessionClose() {
showSessionResults = false;
}
function cancelReset() {
showResetConfirmation = false;
}
function nextQuestion() {
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
generateQuestion();
@@ -776,27 +699,8 @@
return `/images/flags/${flag.path}`;
}
function handleAchievementsUnlocked() {
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
}
// Global statistics functions
function loadGlobalStats() {
sharedLoadGlobalStats("globalQuizStats");
}
function updateGlobalStats(isCorrect, isSkipped = false) {
sharedUpdateGlobalStats("globalQuizStats", "capitalsQuiz", isCorrect, isSkipped);
}
// Sound functions
function playCorrectSound() {
sharedPlayCorrectSound(soundEnabled);
}
function playWrongSound() {
sharedPlayWrongSound(soundEnabled);
achievementCount = updateAchievementCount(achievementsComponent);
}
</script>
@@ -805,7 +709,7 @@
</svelte:head>
<Header
{theme}
theme={$themeStore}
{setTheme}
{gameStats}
{achievementCount}
@@ -830,7 +734,6 @@
on:settingsChange={handleSettingsChange}
on:settingsToggle={handleSettingsToggle}
on:resetConfirmation={handleResetConfirmation}
on:resetStats={handleResetStats}
/>
<!-- Achievements Component -->
@@ -839,9 +742,9 @@
{gameStats}
{currentStreak}
show={showAchievements}
capitalsCorrect={capitalsCorrect}
perfectRounds={perfectRounds}
mapChallengeCompleted={mapChallengeCompleted}
{capitalsCorrect}
{perfectRounds}
{mapChallengeCompleted}
on:close={() => (showAchievements = false)}
on:achievementsUnlocked={handleAchievementsUnlocked}
/>
@@ -853,7 +756,7 @@
{sessionStats}
{sessionLength}
{showSessionResults}
quizInfo={quizInfo}
{quizInfo}
on:startQuiz={startNewSession}
on:openSettings={() => (showSettings = true)}
on:closeResults={() => (showSessionResults = false)}
@@ -900,8 +803,11 @@
<button
class="option"
class:selected={selectedAnswer === index}
class:correct={showResult && index === currentQuestion.correctIndex}
class:wrong={showResult && selectedAnswer === index && index !== currentQuestion.correctIndex}
class:correct={showResult &&
index === currentQuestion.correctIndex}
class:wrong={showResult &&
selectedAnswer === index &&
index !== currentQuestion.correctIndex}
on:click={() => selectAnswer(index)}
disabled={gameState === "answered"}
>
@@ -911,9 +817,14 @@
</div>
{#if gameState === "question"}
<button class="btn btn-skip btn-next-full" on:click={skipQuestion}>Skip Question</button>
<button class="btn btn-skip btn-next-full" on:click={skipQuestion}
>Skip Question</button
>
{:else if (!autoAdvance && gameState === "answered") || (autoAdvance && gameState === "answered" && sessionRestoredFromReload)}
<button class="btn btn-primary btn-next-full" on:click={nextQuestion}>Next Question →</button>
<button
class="btn btn-primary btn-next-full"
on:click={nextQuestion}>Next Question →</button
>
{/if}
<!-- Auto-advance timer display -->

View File

@@ -1,7 +1,21 @@
<script>
import { updateAchievementCount as sharedUpdateAchievementCount } from '../quizLogic/quizAchievements.js';
import { saveSettings as sharedSaveSettings } from '../quizLogic/quizSettings.js';
import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
import { updateAchievementCount } from "../quizLogic/quizAchievements.js";
import { saveSettings } from "../quizLogic/quizSettings.js";
import {
loadGlobalStats,
updateGlobalStats,
} from "../quizLogic/quizGlobalStats.js";
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
import {
saveSessionState,
loadSessionState,
clearSessionState,
createNewSessionState,
} from "../quizLogic/quizSession.js";
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
import { quizInfo } from "../quizInfo/FlagQuizInfo.js";
import { onMount } from "svelte";
import Header from "../components/Header.svelte";
import Footer from "../components/Footer.svelte";
@@ -33,11 +47,10 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
let showCountryInfo = false;
let showResultCountryInfo = false;
// Auto-advance timer variables
let autoAdvanceTimer = null;
// advance timer (shared)
let advanceTimer;
let timerProgress = 0;
let timerDuration = 2000; // 2 seconds
let timerStartTime = 0;
// Force component re-render key to prevent button state persistence
let questionKey = 0;
@@ -78,23 +91,14 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
let sessionStartTime = null;
let sessionRestoredFromReload = false; // Track if session was restored from page reload
// Theme
let theme = "system";
function setTheme(t) {
localStorage.setItem("theme", t);
applyTheme(t);
theme = t;
}
// Update achievement count when achievements component is available
$: if (achievementsComponent) {
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
achievementCount = updateAchievementCount(achievementsComponent);
}
// Save settings when they change (after initial load)
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
sharedSaveSettings("flagQuizSettings", {
saveSettings("flagQuizSettings", {
autoAdvance,
focusWrongAnswers,
reduceCorrectAnswers,
@@ -105,9 +109,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
// Load game stats from localStorage
onMount(async () => {
// Initialize theme
theme = localStorage.getItem("theme") || "system";
applyTheme(theme);
applyTheme($themeStore);
// Set window.appData for header compatibility
if (typeof window !== "undefined") {
@@ -115,8 +117,8 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
...window.appData,
collection: "flags",
setCollection: () => {},
theme,
setTheme,
theme: $themeStore,
setTheme: setTheme,
};
// Load saved game stats
@@ -185,26 +187,50 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
}
// Load global stats and update them
loadGlobalStats();
loadGlobalStats("globalQuizStats");
}
await loadFlags();
settingsLoaded = true;
// Load or initialize session
loadSessionState();
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;
sessionRestoredFromReload = true;
if (!currentQuestion) {
generateQuestion();
}
} else {
quizSubpage = "welcome";
gameState = "welcome";
}
} else {
quizSubpage = "welcome";
gameState = "welcome";
}
});
function applyTheme(theme) {
let effectiveTheme = theme;
if (theme === "system") {
effectiveTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
document.documentElement.setAttribute("data-theme", effectiveTheme);
document.documentElement.className = effectiveTheme;
}
// use shared applyTheme from ../utils/theme.js
async function loadFlags() {
try {
@@ -239,76 +265,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
}
}
function saveSessionState() {
const sessionState = {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
};
localStorage.setItem("flagQuizSessionState", JSON.stringify(sessionState));
}
function loadSessionState() {
const savedState = localStorage.getItem("flagQuizSessionState");
if (savedState) {
try {
const state = JSON.parse(savedState);
if (state.sessionInProgress) {
// Restore session
sessionInProgress = state.sessionInProgress;
currentSessionQuestions = state.currentSessionQuestions || 0;
sessionStats = state.sessionStats || {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength,
};
score = state.score || { correct: 0, total: 0, skipped: 0 };
currentQuestion = state.currentQuestion;
selectedAnswer = state.selectedAnswer;
showResult = state.showResult || false;
gameState = state.gameState || "question";
quizSubpage = "quiz";
sessionStartTime = state.sessionStartTime;
questionKey = state.questionKey || 0;
// Mark that session was restored from reload
sessionRestoredFromReload = true;
// If we don't have a current question, generate one
if (!currentQuestion) {
generateQuestion();
}
} else {
// No active session, show welcome page
quizSubpage = "welcome";
gameState = "welcome";
}
} catch (e) {
console.error("Error loading session state:", e);
quizSubpage = "welcome";
gameState = "welcome";
}
} else {
// No saved state, show welcome page
quizSubpage = "welcome";
gameState = "welcome";
}
}
function clearSessionState() {
localStorage.removeItem("flagQuizSessionState");
}
function generateQuestion() {
if (flags.length < 4) {
console.error("Not enough flags to generate question");
@@ -430,7 +386,19 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
console.log("Generated question:", currentQuestion);
// Save session state
saveSessionState();
saveSessionState("flagQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
}
function selectAnswer(index) {
if (gameState !== "question") return;
@@ -454,7 +422,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
currentStreak++;
// Play correct sound
playCorrectSound();
playCorrectSound(soundEnabled);
// Track correct answer for this flag
if (currentQuestion.correct?.name) {
@@ -494,7 +462,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
currentStreak = 0; // Reset streak on wrong answer
// Play wrong sound
playWrongSound();
playWrongSound(soundEnabled);
// Track wrong answer for this flag
if (currentQuestion.correct?.name) {
@@ -517,7 +485,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
// Update global stats
updateGlobalStats(isCorrect);
updateGlobalStats("globalQuizStats", "flagQuiz", isCorrect);
// Check for new achievements
if (achievementsComponent) {
@@ -525,7 +493,19 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
}
// Save session state
saveSessionState();
saveSessionState("flagQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
// Check if session is complete
if (currentSessionQuestions >= sessionLength) {
@@ -576,10 +556,22 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
// Update global stats (skipped question)
updateGlobalStats(null, true);
updateGlobalStats("globalQuizStats", "flagQuiz", null, true);
// Save session state
saveSessionState();
saveSessionState("flagQuizSessionState", {
sessionInProgress,
currentSessionQuestions,
sessionStats,
score,
currentQuestion,
selectedAnswer,
showResult,
gameState,
quizSubpage,
sessionStartTime,
questionKey,
});
// Check if session is complete
if (currentSessionQuestions >= sessionLength) {
@@ -595,64 +587,40 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
function startAutoAdvanceTimer(duration) {
timerDuration = duration;
timerProgress = 0;
timerStartTime = Date.now();
// Clear any existing timer
if (autoAdvanceTimer) {
clearInterval(autoAdvanceTimer);
if (!advanceTimer) {
advanceTimer = createAdvanceTimer(
(p) => (timerProgress = p),
() => generateQuestion(),
);
}
// Update progress every 50ms for smooth animation
autoAdvanceTimer = setInterval(() => {
const elapsed = Date.now() - timerStartTime;
timerProgress = Math.min((elapsed / duration) * 100, 100);
if (timerProgress >= 100) {
clearInterval(autoAdvanceTimer);
autoAdvanceTimer = null;
timerProgress = 0;
generateQuestion();
}
}, 50);
advanceTimer.start(duration);
}
function cancelAutoAdvanceTimer() {
if (autoAdvanceTimer) {
clearInterval(autoAdvanceTimer);
autoAdvanceTimer = null;
timerProgress = 0;
}
if (advanceTimer) advanceTimer.cancel();
timerProgress = 0;
}
function startNewSession() {
// Reset session data
score = { correct: 0, total: 0, skipped: 0 };
currentSessionQuestions = 0;
sessionStats = {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength,
};
sessionInProgress = true;
sessionStartTime = Date.now();
showSessionResults = false;
sessionRestoredFromReload = false; // Reset reload flag for new sessions
// Create a canonical new session state and apply it to component state
const s = createNewSessionState(sessionLength);
score = s.score;
currentSessionQuestions = s.currentSessionQuestions;
sessionStats = s.sessionStats;
sessionInProgress = s.sessionInProgress;
sessionStartTime = s.sessionStartTime;
showSessionResults = s.showSessionResults;
sessionRestoredFromReload = s.sessionRestoredFromReload;
// Switch to quiz subpage
quizSubpage = "quiz";
gameState = "loading";
// Generate first question
generateQuestion();
}
function endSession() {
// Clear session state
sessionInProgress = false;
clearSessionState();
clearSessionState("flagQuizSessionState");
// Switch to welcome/stats page
quizSubpage = "welcome";
@@ -660,20 +628,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
showSessionResults = true; // Show results on welcome page
}
function resetGame() {
endSession();
}
function resetStats() {
gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
}
function toggleSettings() {
showSettings = !showSettings;
}
function handleSettingsChange(event) {
const {
autoAdvance: newAutoAdvance,
@@ -700,53 +654,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
showResetConfirmation = event.detail;
}
function handleResetStats() {
// Reset game statistics
gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
score = { correct: 0, total: 0, skipped: 0 };
currentStreak = 0;
currentSessionQuestions = 0;
sessionStats = {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength,
};
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
// Reset wrong answers tracking
wrongAnswers = new Map();
localStorage.removeItem("flagQuizWrongAnswers");
// Reset correct answers tracking
correctAnswers = new Map();
localStorage.removeItem("flagQuizCorrectAnswers");
// Reset achievements if component is available
if (achievementsComponent) {
achievementsComponent.resetConsecutiveSkips();
}
showResetConfirmation = false;
}
function handleSessionPlayAgain() {
resetGame();
}
function handleSessionGoToGames() {
window.location.hash = "#/game";
}
function handleSessionClose() {
showSessionResults = false;
}
function cancelReset() {
showResetConfirmation = false;
}
function nextQuestion() {
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
generateQuestion();
@@ -784,129 +691,8 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
return `/images/flags/${flag.path}`;
}
function handleAchievementsUnlocked() {
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
}
// Global statistics functions
function loadGlobalStats() {
const savedGlobalStats = localStorage.getItem("globalQuizStats");
if (savedGlobalStats) {
try {
const globalStats = JSON.parse(savedGlobalStats);
console.log("Loaded global stats:", globalStats);
} catch (e) {
console.error("Error loading global stats:", e);
}
}
}
function updateGlobalStats(isCorrect, isSkipped = false) {
let globalStats = {};
// Load existing global stats
const savedGlobalStats = localStorage.getItem("globalQuizStats");
if (savedGlobalStats) {
try {
globalStats = JSON.parse(savedGlobalStats);
} catch (e) {
console.error("Error parsing global stats:", e);
}
}
// Initialize stats structure if it doesn't exist
if (!globalStats.flagQuiz) {
globalStats.flagQuiz = { correct: 0, wrong: 0, total: 0, skipped: 0 };
}
if (!globalStats.overall) {
globalStats.overall = { correct: 0, wrong: 0, total: 0, skipped: 0 };
}
// Update flag quiz stats
globalStats.flagQuiz.total++;
globalStats.overall.total++;
if (isSkipped) {
globalStats.flagQuiz.skipped++;
globalStats.overall.skipped++;
} else if (isCorrect) {
globalStats.flagQuiz.correct++;
globalStats.overall.correct++;
} else {
globalStats.flagQuiz.wrong++;
globalStats.overall.wrong++;
}
// Save updated global stats
localStorage.setItem("globalQuizStats", JSON.stringify(globalStats));
console.log("Updated global stats:", globalStats);
}
// Sound functions
function playCorrectSound() {
if (!soundEnabled) return;
try {
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Pleasant ascending tone for correct answer
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(
659.25,
audioContext.currentTime + 0.1,
); // E5
oscillator.frequency.setValueAtTime(
783.99,
audioContext.currentTime + 0.2,
); // G5
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.001,
audioContext.currentTime + 0.4,
);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.4);
} catch (e) {
console.log("Audio not supported:", e);
}
}
function playWrongSound() {
if (!soundEnabled) return;
try {
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Descending tone for wrong answer
oscillator.frequency.setValueAtTime(400, audioContext.currentTime); // Lower frequency
oscillator.frequency.setValueAtTime(300, audioContext.currentTime + 0.15);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.001,
audioContext.currentTime + 0.3,
);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (e) {
console.log("Audio not supported:", e);
}
achievementCount = updateAchievementCount(achievementsComponent);
}
</script>
@@ -915,7 +701,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
</svelte:head>
<Header
{theme}
theme={$themeStore}
{setTheme}
{gameStats}
{achievementCount}
@@ -940,7 +726,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
on:settingsChange={handleSettingsChange}
on:settingsToggle={handleSettingsToggle}
on:resetConfirmation={handleResetConfirmation}
on:resetStats={handleResetStats}
/>
<!-- Achievements Component -->
@@ -955,12 +740,12 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
{#if quizSubpage === "welcome"}
<!-- Welcome/Stats Subpage -->
<QuizInfo
<QuizInfo
{gameStats}
{sessionStats}
{sessionLength}
{showSessionResults}
quizInfo={quizInfo}
{quizInfo}
on:startQuiz={startNewSession}
on:openSettings={() => (showSettings = true)}
on:closeResults={() => (showSessionResults = false)}

View File

@@ -1,4 +1,5 @@
<script>
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
import { onMount } from 'svelte';
import { collections } from '../collections.js';
import Header from '../components/Header.svelte';
@@ -12,13 +13,7 @@
let availableGames = [flagQuizInfo, capitalsQuizInfo, geographyQuizInfo, logoQuizInfo, emblemQuizInfo];
let theme = 'system';
function setTheme(t) {
localStorage.setItem('theme', t);
applyTheme(t);
theme = t;
}
let theme;
onMount(() => {
// Initialize theme from storage and apply
@@ -32,22 +27,14 @@
setCollection: () => {},
collections: collections,
theme,
setTheme
setTheme
};
}
});
function applyTheme(theme) {
let effectiveTheme = theme;
if (theme === 'system') {
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', effectiveTheme);
document.documentElement.className = effectiveTheme;
}
</script>
<Header {theme} {setTheme} />
<Header theme={$themeStore} setTheme={setTheme} />
<main class="game-selection">
<div class="container">

View File

@@ -0,0 +1,56 @@
// Shared advance timer for quizzes
// createAdvanceTimer(onProgress, onComplete) -> { start(duration), cancel() }
export function createAdvanceTimer(onProgress, onComplete) {
let timer = null;
let startTime = 0;
let duration = 0;
function _clear() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
return {
start(d) {
duration = d || 0;
// reset progress immediately
try {
onProgress(0);
} catch (e) {
// ignore
}
startTime = Date.now();
_clear();
timer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min((elapsed / duration) * 100, 100);
try {
onProgress(progress);
} catch (e) {
// ignore
}
if (progress >= 100) {
_clear();
try {
onProgress(0);
} catch (e) {}
if (typeof onComplete === 'function') {
try {
onComplete();
} catch (e) {
console.error('advanceTimer onComplete error', e);
}
}
}
}, 50);
},
cancel() {
_clear();
try {
onProgress(0);
} catch (e) {}
},
};
}

View File

@@ -21,3 +21,22 @@ export function loadSessionState(key, defaultState) {
export function clearSessionState(key) {
localStorage.removeItem(key);
}
// Return a new session state object for a quiz with the provided session length
export function createNewSessionState(sessionLength = 10) {
return {
score: { correct: 0, total: 0, skipped: 0 },
currentSessionQuestions: 0,
sessionStats: {
correct: 0,
wrong: 0,
skipped: 0,
total: 0,
sessionLength: sessionLength,
},
sessionInProgress: true,
sessionStartTime: Date.now(),
showSessionResults: false,
sessionRestoredFromReload: false,
};
}

29
src/utils/theme.js Normal file
View File

@@ -0,0 +1,29 @@
export function applyTheme(theme) {
let effectiveTheme = theme;
if (theme === "system") {
effectiveTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
document.documentElement.setAttribute("data-theme", effectiveTheme);
document.documentElement.className = effectiveTheme;
console.log("[Theme] Applied theme:", effectiveTheme);
}
export function setTheme(newTheme) {
if (newTheme === "light" || newTheme === "dark" || newTheme === "system") {
// Persist choice and apply immediately
localStorage.setItem("theme", newTheme);
console.log("[Theme] setTheme:", newTheme);
setTimeout(() => applyTheme(newTheme), 0);
return newTheme;
}
}
export function getStoredTheme(defaultTheme = "system") {
return (typeof localStorage !== "undefined" && localStorage.getItem("theme")) || defaultTheme;
}
// Svelte store for reactive theme across components
import { writable } from 'svelte/store';
export const themeStore = writable(getStoredTheme());