mirror of
https://github.com/shadoll/sLogos.git
synced 2026-02-04 02:53:22 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
56
src/quizLogic/advanceTimer.js
Normal file
56
src/quizLogic/advanceTimer.js
Normal 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) {}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
29
src/utils/theme.js
Normal 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());
|
||||
Reference in New Issue
Block a user