mirror of
https://github.com/shadoll/sLogos.git
synced 2026-08-28 11:33:29 +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:
+6
-27
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { collections } from "./collections.js";
|
import { collections } from "./collections.js";
|
||||||
|
import { applyTheme, setTheme, themeStore } from "./utils/theme.js";
|
||||||
import Router from "svelte-spa-router/Router.svelte";
|
import Router from "svelte-spa-router/Router.svelte";
|
||||||
import Home from "./pages/Home.svelte";
|
import Home from "./pages/Home.svelte";
|
||||||
import Preview from "./pages/Preview.svelte";
|
import Preview from "./pages/Preview.svelte";
|
||||||
@@ -26,7 +27,9 @@
|
|||||||
let logos = [];
|
let logos = [];
|
||||||
let filteredLogos = [];
|
let filteredLogos = [];
|
||||||
let displayLogos = [];
|
let displayLogos = [];
|
||||||
let theme = "system";
|
let theme;
|
||||||
|
// Keep local theme in sync with shared theme store
|
||||||
|
$: theme = $themeStore;
|
||||||
let allTags = [];
|
let allTags = [];
|
||||||
let selectedTags = [];
|
let selectedTags = [];
|
||||||
let selectedBrands = [];
|
let selectedBrands = [];
|
||||||
@@ -178,7 +181,7 @@
|
|||||||
logos: [],
|
logos: [],
|
||||||
filteredLogos: [],
|
filteredLogos: [],
|
||||||
displayLogos: [],
|
displayLogos: [],
|
||||||
theme,
|
theme: $themeStore,
|
||||||
effectiveTheme: "light",
|
effectiveTheme: "light",
|
||||||
viewMode,
|
viewMode,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -543,30 +546,6 @@
|
|||||||
document.body.removeChild(link);
|
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) {
|
function toggleTag(tag) {
|
||||||
console.log("App: Toggling tag:", tag);
|
console.log("App: Toggling tag:", tag);
|
||||||
if (selectedTags.includes(tag)) {
|
if (selectedTags.includes(tag)) {
|
||||||
@@ -841,7 +820,7 @@
|
|||||||
{displayLogos}
|
{displayLogos}
|
||||||
allLogos={logos}
|
allLogos={logos}
|
||||||
{theme}
|
{theme}
|
||||||
{setTheme}
|
setTheme={setTheme}
|
||||||
{viewMode}
|
{viewMode}
|
||||||
{setGridView}
|
{setGridView}
|
||||||
{setListView}
|
{setListView}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleOverlayClick(event) {
|
function handleOverlayClick(event) {
|
||||||
// Only close if clicking on the overlay itself, not the modal content
|
|
||||||
if (event.target === event.currentTarget) {
|
if (event.target === event.currentTarget) {
|
||||||
toggleSettings();
|
toggleSettings();
|
||||||
}
|
}
|
||||||
@@ -41,8 +40,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleResetStats() {
|
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;
|
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
|
// Reactive statements to dispatch settings changes
|
||||||
|
|||||||
+148
-237
@@ -1,14 +1,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import { updateAchievementCount as sharedUpdateAchievementCount } from '../quizLogic/quizAchievements.js';
|
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
|
||||||
import { saveSettings as sharedSaveSettings, loadSettings as sharedLoadSettings } from '../quizLogic/quizSettings.js';
|
|
||||||
import { loadGlobalStats as sharedLoadGlobalStats, updateGlobalStats as sharedUpdateGlobalStats } from '../quizLogic/quizGlobalStats.js';
|
import { updateAchievementCount } from "../quizLogic/quizAchievements.js";
|
||||||
import { saveSessionState as sharedSaveSessionState, loadSessionState as sharedLoadSessionState, clearSessionState as sharedClearSessionState } from '../quizLogic/quizSession.js';
|
import { saveSettings, loadSettings } from "../quizLogic/quizSettings.js";
|
||||||
import { playCorrectSound as sharedPlayCorrectSound, playWrongSound as sharedPlayWrongSound } from '../quizLogic/quizSound.js';
|
import {
|
||||||
import { quizInfo } from '../quizInfo/CapitalsQuizInfo.js';
|
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 { onMount } from "svelte";
|
||||||
import Header from "../components/Header.svelte";
|
import Header from "../components/Header.svelte";
|
||||||
import Footer from "../components/Footer.svelte";
|
import Footer from "../components/Footer.svelte";
|
||||||
import InlineSvg from "../components/InlineSvg.svelte";
|
|
||||||
import Achievements from "../components/Achievements.svelte";
|
import Achievements from "../components/Achievements.svelte";
|
||||||
import QuizSettings from "../components/QuizSettings.svelte";
|
import QuizSettings from "../components/QuizSettings.svelte";
|
||||||
import QuizInfo from "../components/QuizInfo.svelte";
|
import QuizInfo from "../components/QuizInfo.svelte";
|
||||||
@@ -34,10 +44,11 @@
|
|||||||
let showResultCountryInfo = false;
|
let showResultCountryInfo = false;
|
||||||
|
|
||||||
// Auto-advance timer variables
|
// Auto-advance timer variables
|
||||||
let autoAdvanceTimer = null;
|
// advance timer (shared)
|
||||||
|
|
||||||
|
let advanceTimer;
|
||||||
let timerProgress = 0;
|
let timerProgress = 0;
|
||||||
let timerDuration = 2000; // 2 seconds
|
let timerDuration = 2000; // 2 seconds
|
||||||
let timerStartTime = 0;
|
|
||||||
|
|
||||||
// Force component re-render key to prevent button state persistence
|
// Force component re-render key to prevent button state persistence
|
||||||
let questionKey = 0;
|
let questionKey = 0;
|
||||||
@@ -81,23 +92,14 @@
|
|||||||
let sessionStartTime = null;
|
let sessionStartTime = null;
|
||||||
let sessionRestoredFromReload = false; // Track if session was restored from page reload
|
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
|
// Update achievement count when achievements component is available
|
||||||
$: if (achievementsComponent) {
|
$: if (achievementsComponent) {
|
||||||
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(achievementsComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings when they change (after initial load)
|
// Save settings when they change (after initial load)
|
||||||
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
|
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
|
||||||
sharedSaveSettings("capitalsQuizSettings", {
|
saveSettings("capitalsQuizSettings", {
|
||||||
autoAdvance,
|
autoAdvance,
|
||||||
focusWrongAnswers,
|
focusWrongAnswers,
|
||||||
reduceCorrectAnswers,
|
reduceCorrectAnswers,
|
||||||
@@ -108,9 +110,7 @@
|
|||||||
|
|
||||||
// Load game stats from localStorage
|
// Load game stats from localStorage
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Initialize theme
|
applyTheme($themeStore);
|
||||||
theme = localStorage.getItem("theme") || "system";
|
|
||||||
applyTheme(theme);
|
|
||||||
|
|
||||||
// Set window.appData for header compatibility
|
// Set window.appData for header compatibility
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -118,8 +118,8 @@
|
|||||||
...window.appData,
|
...window.appData,
|
||||||
collection: "capitals",
|
collection: "capitals",
|
||||||
setCollection: () => {},
|
setCollection: () => {},
|
||||||
theme,
|
theme: $themeStore,
|
||||||
setTheme,
|
setTheme: setTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load saved game stats
|
// Load saved game stats
|
||||||
@@ -140,7 +140,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load wrong answers tracking
|
// Load wrong answers tracking
|
||||||
const savedWrongAnswers = localStorage.getItem("capitalsQuizWrongAnswers");
|
const savedWrongAnswers = localStorage.getItem(
|
||||||
|
"capitalsQuizWrongAnswers",
|
||||||
|
);
|
||||||
if (savedWrongAnswers) {
|
if (savedWrongAnswers) {
|
||||||
try {
|
try {
|
||||||
const loadedWrongAnswers = JSON.parse(savedWrongAnswers);
|
const loadedWrongAnswers = JSON.parse(savedWrongAnswers);
|
||||||
@@ -164,7 +166,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
const loadedSettings = sharedLoadSettings("capitalsQuizSettings", {
|
const loadedSettings = loadSettings("capitalsQuizSettings", {
|
||||||
autoAdvance,
|
autoAdvance,
|
||||||
focusWrongAnswers,
|
focusWrongAnswers,
|
||||||
reduceCorrectAnswers,
|
reduceCorrectAnswers,
|
||||||
@@ -180,80 +182,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load global stats and update them
|
// Load global stats and update them
|
||||||
sharedLoadGlobalStats("globalQuizStats");
|
loadGlobalStats("globalQuizStats");
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadFlags();
|
await loadFlags();
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
|
||||||
// Load or initialize session
|
// Load or initialize session
|
||||||
loadSessionState();
|
const loadedSession = loadSessionState("capitalsQuizSessionState", null);
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
if (loadedSession) {
|
if (loadedSession) {
|
||||||
// Restore session
|
// Restore session
|
||||||
sessionInProgress = loadedSession.sessionInProgress;
|
sessionInProgress = loadedSession.sessionInProgress;
|
||||||
@@ -286,10 +222,42 @@
|
|||||||
quizSubpage = "welcome";
|
quizSubpage = "welcome";
|
||||||
gameState = "welcome";
|
gameState = "welcome";
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSessionState() {
|
flags = uniqueFlags;
|
||||||
sharedClearSessionState("capitalsQuizSessionState");
|
console.log(
|
||||||
|
`Loaded ${flags.length} unique country flags for capitals quiz`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading flags:", error);
|
||||||
|
flags = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQuestion() {
|
function generateQuestion() {
|
||||||
@@ -410,7 +378,19 @@
|
|||||||
console.log("Generated capitals question:", currentQuestion);
|
console.log("Generated capitals question:", currentQuestion);
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("capitalsQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAnswer(index) {
|
function selectAnswer(index) {
|
||||||
@@ -436,7 +416,7 @@
|
|||||||
capitalsCorrect++;
|
capitalsCorrect++;
|
||||||
|
|
||||||
// Play correct sound
|
// Play correct sound
|
||||||
playCorrectSound();
|
playCorrectSound(soundEnabled);
|
||||||
|
|
||||||
// Track correct answer for this flag
|
// Track correct answer for this flag
|
||||||
if (currentQuestion.correct?.name) {
|
if (currentQuestion.correct?.name) {
|
||||||
@@ -476,7 +456,7 @@
|
|||||||
currentStreak = 0; // Reset streak on wrong answer
|
currentStreak = 0; // Reset streak on wrong answer
|
||||||
|
|
||||||
// Play wrong sound
|
// Play wrong sound
|
||||||
playWrongSound();
|
playWrongSound(soundEnabled);
|
||||||
|
|
||||||
// Track wrong answer for this flag
|
// Track wrong answer for this flag
|
||||||
if (currentQuestion.correct?.name) {
|
if (currentQuestion.correct?.name) {
|
||||||
@@ -499,7 +479,7 @@
|
|||||||
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
|
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
|
||||||
|
|
||||||
// Update global stats
|
// Update global stats
|
||||||
updateGlobalStats(isCorrect);
|
updateGlobalStats("globalQuizStats", "capitalsQuiz", isCorrect);
|
||||||
|
|
||||||
// Check for new achievements
|
// Check for new achievements
|
||||||
if (achievementsComponent) {
|
if (achievementsComponent) {
|
||||||
@@ -507,7 +487,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("capitalsQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if session is complete
|
// Check if session is complete
|
||||||
if (currentSessionQuestions >= sessionLength) {
|
if (currentSessionQuestions >= sessionLength) {
|
||||||
@@ -559,10 +551,22 @@
|
|||||||
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
|
localStorage.setItem("capitalsQuizStats", JSON.stringify(gameStats));
|
||||||
|
|
||||||
// Update global stats (skipped question)
|
// Update global stats (skipped question)
|
||||||
updateGlobalStats(null, true);
|
updateGlobalStats("globalQuizStats", "capitalsQuiz", null, true);
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("capitalsQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if session is complete
|
// Check if session is complete
|
||||||
if (currentSessionQuestions >= sessionLength) {
|
if (currentSessionQuestions >= sessionLength) {
|
||||||
@@ -578,69 +582,49 @@
|
|||||||
|
|
||||||
function startAutoAdvanceTimer(duration) {
|
function startAutoAdvanceTimer(duration) {
|
||||||
timerDuration = duration;
|
timerDuration = duration;
|
||||||
timerProgress = 0;
|
if (!advanceTimer) {
|
||||||
timerStartTime = Date.now();
|
advanceTimer = createAdvanceTimer(
|
||||||
|
(p) => (timerProgress = p),
|
||||||
// Clear any existing timer
|
() => generateQuestion(),
|
||||||
if (autoAdvanceTimer) {
|
);
|
||||||
clearInterval(autoAdvanceTimer);
|
|
||||||
}
|
}
|
||||||
|
advanceTimer.start(duration);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAutoAdvanceTimer() {
|
function cancelAutoAdvanceTimer() {
|
||||||
if (autoAdvanceTimer) {
|
if (advanceTimer) advanceTimer.cancel();
|
||||||
clearInterval(autoAdvanceTimer);
|
|
||||||
autoAdvanceTimer = null;
|
|
||||||
timerProgress = 0;
|
timerProgress = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function startNewSession() {
|
function startNewSession() {
|
||||||
// Reset session data
|
// Create canonical new session state and apply it
|
||||||
score = { correct: 0, total: 0, skipped: 0 };
|
const s = createNewSessionState(sessionLength);
|
||||||
currentSessionQuestions = 0;
|
score = s.score;
|
||||||
sessionStats = {
|
currentSessionQuestions = s.currentSessionQuestions;
|
||||||
correct: 0,
|
sessionStats = s.sessionStats;
|
||||||
wrong: 0,
|
sessionInProgress = s.sessionInProgress;
|
||||||
skipped: 0,
|
sessionStartTime = s.sessionStartTime;
|
||||||
total: 0,
|
showSessionResults = s.showSessionResults;
|
||||||
sessionLength,
|
sessionRestoredFromReload = s.sessionRestoredFromReload;
|
||||||
};
|
|
||||||
sessionInProgress = true;
|
|
||||||
sessionStartTime = Date.now();
|
|
||||||
showSessionResults = false;
|
|
||||||
sessionRestoredFromReload = false; // Reset reload flag for new sessions
|
|
||||||
|
|
||||||
// Switch to quiz subpage
|
|
||||||
quizSubpage = "quiz";
|
quizSubpage = "quiz";
|
||||||
gameState = "loading";
|
gameState = "loading";
|
||||||
|
|
||||||
// Generate first question
|
|
||||||
generateQuestion();
|
generateQuestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
function endSession() {
|
function endSession() {
|
||||||
// Track perfect rounds for achievements
|
// 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++;
|
perfectRounds++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear session state
|
// Clear session state
|
||||||
sessionInProgress = false;
|
sessionInProgress = false;
|
||||||
clearSessionState();
|
clearSessionState("capitalsQuizSessionState");
|
||||||
|
|
||||||
// Switch to welcome/stats page
|
// Switch to welcome/stats page
|
||||||
quizSubpage = "welcome";
|
quizSubpage = "welcome";
|
||||||
@@ -648,20 +632,6 @@
|
|||||||
showSessionResults = true; // Show results on welcome page
|
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) {
|
function handleSettingsChange(event) {
|
||||||
const {
|
const {
|
||||||
autoAdvance: newAutoAdvance,
|
autoAdvance: newAutoAdvance,
|
||||||
@@ -688,53 +658,6 @@
|
|||||||
showResetConfirmation = event.detail;
|
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() {
|
function nextQuestion() {
|
||||||
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
|
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
|
||||||
generateQuestion();
|
generateQuestion();
|
||||||
@@ -776,27 +699,8 @@
|
|||||||
return `/images/flags/${flag.path}`;
|
return `/images/flags/${flag.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleAchievementsUnlocked() {
|
function handleAchievementsUnlocked() {
|
||||||
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(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);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -805,7 +709,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Header
|
<Header
|
||||||
{theme}
|
theme={$themeStore}
|
||||||
{setTheme}
|
{setTheme}
|
||||||
{gameStats}
|
{gameStats}
|
||||||
{achievementCount}
|
{achievementCount}
|
||||||
@@ -830,7 +734,6 @@
|
|||||||
on:settingsChange={handleSettingsChange}
|
on:settingsChange={handleSettingsChange}
|
||||||
on:settingsToggle={handleSettingsToggle}
|
on:settingsToggle={handleSettingsToggle}
|
||||||
on:resetConfirmation={handleResetConfirmation}
|
on:resetConfirmation={handleResetConfirmation}
|
||||||
on:resetStats={handleResetStats}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Achievements Component -->
|
<!-- Achievements Component -->
|
||||||
@@ -839,9 +742,9 @@
|
|||||||
{gameStats}
|
{gameStats}
|
||||||
{currentStreak}
|
{currentStreak}
|
||||||
show={showAchievements}
|
show={showAchievements}
|
||||||
capitalsCorrect={capitalsCorrect}
|
{capitalsCorrect}
|
||||||
perfectRounds={perfectRounds}
|
{perfectRounds}
|
||||||
mapChallengeCompleted={mapChallengeCompleted}
|
{mapChallengeCompleted}
|
||||||
on:close={() => (showAchievements = false)}
|
on:close={() => (showAchievements = false)}
|
||||||
on:achievementsUnlocked={handleAchievementsUnlocked}
|
on:achievementsUnlocked={handleAchievementsUnlocked}
|
||||||
/>
|
/>
|
||||||
@@ -853,7 +756,7 @@
|
|||||||
{sessionStats}
|
{sessionStats}
|
||||||
{sessionLength}
|
{sessionLength}
|
||||||
{showSessionResults}
|
{showSessionResults}
|
||||||
quizInfo={quizInfo}
|
{quizInfo}
|
||||||
on:startQuiz={startNewSession}
|
on:startQuiz={startNewSession}
|
||||||
on:openSettings={() => (showSettings = true)}
|
on:openSettings={() => (showSettings = true)}
|
||||||
on:closeResults={() => (showSessionResults = false)}
|
on:closeResults={() => (showSessionResults = false)}
|
||||||
@@ -900,8 +803,11 @@
|
|||||||
<button
|
<button
|
||||||
class="option"
|
class="option"
|
||||||
class:selected={selectedAnswer === index}
|
class:selected={selectedAnswer === index}
|
||||||
class:correct={showResult && index === currentQuestion.correctIndex}
|
class:correct={showResult &&
|
||||||
class:wrong={showResult && selectedAnswer === index && index !== currentQuestion.correctIndex}
|
index === currentQuestion.correctIndex}
|
||||||
|
class:wrong={showResult &&
|
||||||
|
selectedAnswer === index &&
|
||||||
|
index !== currentQuestion.correctIndex}
|
||||||
on:click={() => selectAnswer(index)}
|
on:click={() => selectAnswer(index)}
|
||||||
disabled={gameState === "answered"}
|
disabled={gameState === "answered"}
|
||||||
>
|
>
|
||||||
@@ -911,9 +817,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if gameState === "question"}
|
{#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)}
|
{: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}
|
{/if}
|
||||||
|
|
||||||
<!-- Auto-advance timer display -->
|
<!-- Auto-advance timer display -->
|
||||||
|
|||||||
+123
-338
@@ -1,7 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { updateAchievementCount as sharedUpdateAchievementCount } from '../quizLogic/quizAchievements.js';
|
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
|
||||||
import { saveSettings as sharedSaveSettings } from '../quizLogic/quizSettings.js';
|
import { updateAchievementCount } from "../quizLogic/quizAchievements.js";
|
||||||
import { quizInfo } from '../quizInfo/FlagQuizInfo.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 { onMount } from "svelte";
|
||||||
import Header from "../components/Header.svelte";
|
import Header from "../components/Header.svelte";
|
||||||
import Footer from "../components/Footer.svelte";
|
import Footer from "../components/Footer.svelte";
|
||||||
@@ -33,11 +47,10 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
let showCountryInfo = false;
|
let showCountryInfo = false;
|
||||||
let showResultCountryInfo = false;
|
let showResultCountryInfo = false;
|
||||||
|
|
||||||
// Auto-advance timer variables
|
// advance timer (shared)
|
||||||
let autoAdvanceTimer = null;
|
let advanceTimer;
|
||||||
let timerProgress = 0;
|
let timerProgress = 0;
|
||||||
let timerDuration = 2000; // 2 seconds
|
let timerDuration = 2000; // 2 seconds
|
||||||
let timerStartTime = 0;
|
|
||||||
|
|
||||||
// Force component re-render key to prevent button state persistence
|
// Force component re-render key to prevent button state persistence
|
||||||
let questionKey = 0;
|
let questionKey = 0;
|
||||||
@@ -78,23 +91,14 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
let sessionStartTime = null;
|
let sessionStartTime = null;
|
||||||
let sessionRestoredFromReload = false; // Track if session was restored from page reload
|
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
|
// Update achievement count when achievements component is available
|
||||||
$: if (achievementsComponent) {
|
$: if (achievementsComponent) {
|
||||||
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(achievementsComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings when they change (after initial load)
|
// Save settings when they change (after initial load)
|
||||||
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
|
$: if (settingsLoaded && typeof reduceCorrectAnswers !== "undefined") {
|
||||||
sharedSaveSettings("flagQuizSettings", {
|
saveSettings("flagQuizSettings", {
|
||||||
autoAdvance,
|
autoAdvance,
|
||||||
focusWrongAnswers,
|
focusWrongAnswers,
|
||||||
reduceCorrectAnswers,
|
reduceCorrectAnswers,
|
||||||
@@ -105,9 +109,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
|
|
||||||
// Load game stats from localStorage
|
// Load game stats from localStorage
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Initialize theme
|
applyTheme($themeStore);
|
||||||
theme = localStorage.getItem("theme") || "system";
|
|
||||||
applyTheme(theme);
|
|
||||||
|
|
||||||
// Set window.appData for header compatibility
|
// Set window.appData for header compatibility
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -115,8 +117,8 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
...window.appData,
|
...window.appData,
|
||||||
collection: "flags",
|
collection: "flags",
|
||||||
setCollection: () => {},
|
setCollection: () => {},
|
||||||
theme,
|
theme: $themeStore,
|
||||||
setTheme,
|
setTheme: setTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load saved game stats
|
// Load saved game stats
|
||||||
@@ -185,26 +187,50 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load global stats and update them
|
// Load global stats and update them
|
||||||
loadGlobalStats();
|
loadGlobalStats("globalQuizStats");
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadFlags();
|
await loadFlags();
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
|
||||||
// Load or initialize session
|
// 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) {
|
// use shared applyTheme from ../utils/theme.js
|
||||||
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() {
|
async function loadFlags() {
|
||||||
try {
|
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() {
|
function generateQuestion() {
|
||||||
if (flags.length < 4) {
|
if (flags.length < 4) {
|
||||||
console.error("Not enough flags to generate question");
|
console.error("Not enough flags to generate question");
|
||||||
@@ -430,7 +386,19 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
console.log("Generated question:", currentQuestion);
|
console.log("Generated question:", currentQuestion);
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("flagQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function selectAnswer(index) {
|
function selectAnswer(index) {
|
||||||
if (gameState !== "question") return;
|
if (gameState !== "question") return;
|
||||||
@@ -454,7 +422,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
currentStreak++;
|
currentStreak++;
|
||||||
|
|
||||||
// Play correct sound
|
// Play correct sound
|
||||||
playCorrectSound();
|
playCorrectSound(soundEnabled);
|
||||||
|
|
||||||
// Track correct answer for this flag
|
// Track correct answer for this flag
|
||||||
if (currentQuestion.correct?.name) {
|
if (currentQuestion.correct?.name) {
|
||||||
@@ -494,7 +462,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
currentStreak = 0; // Reset streak on wrong answer
|
currentStreak = 0; // Reset streak on wrong answer
|
||||||
|
|
||||||
// Play wrong sound
|
// Play wrong sound
|
||||||
playWrongSound();
|
playWrongSound(soundEnabled);
|
||||||
|
|
||||||
// Track wrong answer for this flag
|
// Track wrong answer for this flag
|
||||||
if (currentQuestion.correct?.name) {
|
if (currentQuestion.correct?.name) {
|
||||||
@@ -517,7 +485,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
|
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
|
||||||
|
|
||||||
// Update global stats
|
// Update global stats
|
||||||
updateGlobalStats(isCorrect);
|
updateGlobalStats("globalQuizStats", "flagQuiz", isCorrect);
|
||||||
|
|
||||||
// Check for new achievements
|
// Check for new achievements
|
||||||
if (achievementsComponent) {
|
if (achievementsComponent) {
|
||||||
@@ -525,7 +493,19 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("flagQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if session is complete
|
// Check if session is complete
|
||||||
if (currentSessionQuestions >= sessionLength) {
|
if (currentSessionQuestions >= sessionLength) {
|
||||||
@@ -576,10 +556,22 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
|
localStorage.setItem("flagQuizStats", JSON.stringify(gameStats));
|
||||||
|
|
||||||
// Update global stats (skipped question)
|
// Update global stats (skipped question)
|
||||||
updateGlobalStats(null, true);
|
updateGlobalStats("globalQuizStats", "flagQuiz", null, true);
|
||||||
|
|
||||||
// Save session state
|
// Save session state
|
||||||
saveSessionState();
|
saveSessionState("flagQuizSessionState", {
|
||||||
|
sessionInProgress,
|
||||||
|
currentSessionQuestions,
|
||||||
|
sessionStats,
|
||||||
|
score,
|
||||||
|
currentQuestion,
|
||||||
|
selectedAnswer,
|
||||||
|
showResult,
|
||||||
|
gameState,
|
||||||
|
quizSubpage,
|
||||||
|
sessionStartTime,
|
||||||
|
questionKey,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if session is complete
|
// Check if session is complete
|
||||||
if (currentSessionQuestions >= sessionLength) {
|
if (currentSessionQuestions >= sessionLength) {
|
||||||
@@ -595,64 +587,40 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
|
|
||||||
function startAutoAdvanceTimer(duration) {
|
function startAutoAdvanceTimer(duration) {
|
||||||
timerDuration = duration;
|
timerDuration = duration;
|
||||||
timerProgress = 0;
|
if (!advanceTimer) {
|
||||||
timerStartTime = Date.now();
|
advanceTimer = createAdvanceTimer(
|
||||||
|
(p) => (timerProgress = p),
|
||||||
// Clear any existing timer
|
() => generateQuestion(),
|
||||||
if (autoAdvanceTimer) {
|
);
|
||||||
clearInterval(autoAdvanceTimer);
|
|
||||||
}
|
}
|
||||||
|
advanceTimer.start(duration);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAutoAdvanceTimer() {
|
function cancelAutoAdvanceTimer() {
|
||||||
if (autoAdvanceTimer) {
|
if (advanceTimer) advanceTimer.cancel();
|
||||||
clearInterval(autoAdvanceTimer);
|
|
||||||
autoAdvanceTimer = null;
|
|
||||||
timerProgress = 0;
|
timerProgress = 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function startNewSession() {
|
function startNewSession() {
|
||||||
// Reset session data
|
// Create a canonical new session state and apply it to component state
|
||||||
score = { correct: 0, total: 0, skipped: 0 };
|
const s = createNewSessionState(sessionLength);
|
||||||
currentSessionQuestions = 0;
|
score = s.score;
|
||||||
sessionStats = {
|
currentSessionQuestions = s.currentSessionQuestions;
|
||||||
correct: 0,
|
sessionStats = s.sessionStats;
|
||||||
wrong: 0,
|
sessionInProgress = s.sessionInProgress;
|
||||||
skipped: 0,
|
sessionStartTime = s.sessionStartTime;
|
||||||
total: 0,
|
showSessionResults = s.showSessionResults;
|
||||||
sessionLength,
|
sessionRestoredFromReload = s.sessionRestoredFromReload;
|
||||||
};
|
|
||||||
sessionInProgress = true;
|
|
||||||
sessionStartTime = Date.now();
|
|
||||||
showSessionResults = false;
|
|
||||||
sessionRestoredFromReload = false; // Reset reload flag for new sessions
|
|
||||||
|
|
||||||
// Switch to quiz subpage
|
|
||||||
quizSubpage = "quiz";
|
quizSubpage = "quiz";
|
||||||
gameState = "loading";
|
gameState = "loading";
|
||||||
|
|
||||||
// Generate first question
|
|
||||||
generateQuestion();
|
generateQuestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
function endSession() {
|
function endSession() {
|
||||||
// Clear session state
|
// Clear session state
|
||||||
sessionInProgress = false;
|
sessionInProgress = false;
|
||||||
clearSessionState();
|
clearSessionState("flagQuizSessionState");
|
||||||
|
|
||||||
// Switch to welcome/stats page
|
// Switch to welcome/stats page
|
||||||
quizSubpage = "welcome";
|
quizSubpage = "welcome";
|
||||||
@@ -660,20 +628,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
showSessionResults = true; // Show results on welcome page
|
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) {
|
function handleSettingsChange(event) {
|
||||||
const {
|
const {
|
||||||
autoAdvance: newAutoAdvance,
|
autoAdvance: newAutoAdvance,
|
||||||
@@ -700,53 +654,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
showResetConfirmation = event.detail;
|
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() {
|
function nextQuestion() {
|
||||||
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
|
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
|
||||||
generateQuestion();
|
generateQuestion();
|
||||||
@@ -784,129 +691,8 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
return `/images/flags/${flag.path}`;
|
return `/images/flags/${flag.path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleAchievementsUnlocked() {
|
function handleAchievementsUnlocked() {
|
||||||
achievementCount = sharedUpdateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -915,7 +701,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Header
|
<Header
|
||||||
{theme}
|
theme={$themeStore}
|
||||||
{setTheme}
|
{setTheme}
|
||||||
{gameStats}
|
{gameStats}
|
||||||
{achievementCount}
|
{achievementCount}
|
||||||
@@ -940,7 +726,6 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
on:settingsChange={handleSettingsChange}
|
on:settingsChange={handleSettingsChange}
|
||||||
on:settingsToggle={handleSettingsToggle}
|
on:settingsToggle={handleSettingsToggle}
|
||||||
on:resetConfirmation={handleResetConfirmation}
|
on:resetConfirmation={handleResetConfirmation}
|
||||||
on:resetStats={handleResetStats}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Achievements Component -->
|
<!-- Achievements Component -->
|
||||||
@@ -960,7 +745,7 @@ import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
|||||||
{sessionStats}
|
{sessionStats}
|
||||||
{sessionLength}
|
{sessionLength}
|
||||||
{showSessionResults}
|
{showSessionResults}
|
||||||
quizInfo={quizInfo}
|
{quizInfo}
|
||||||
on:startQuiz={startNewSession}
|
on:startQuiz={startNewSession}
|
||||||
on:openSettings={() => (showSettings = true)}
|
on:openSettings={() => (showSettings = true)}
|
||||||
on:closeResults={() => (showSessionResults = false)}
|
on:closeResults={() => (showSessionResults = false)}
|
||||||
|
|||||||
+3
-16
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { applyTheme, setTheme, themeStore } from "../utils/theme.js";
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { collections } from '../collections.js';
|
import { collections } from '../collections.js';
|
||||||
import Header from '../components/Header.svelte';
|
import Header from '../components/Header.svelte';
|
||||||
@@ -12,13 +13,7 @@
|
|||||||
|
|
||||||
let availableGames = [flagQuizInfo, capitalsQuizInfo, geographyQuizInfo, logoQuizInfo, emblemQuizInfo];
|
let availableGames = [flagQuizInfo, capitalsQuizInfo, geographyQuizInfo, logoQuizInfo, emblemQuizInfo];
|
||||||
|
|
||||||
let theme = 'system';
|
let theme;
|
||||||
|
|
||||||
function setTheme(t) {
|
|
||||||
localStorage.setItem('theme', t);
|
|
||||||
applyTheme(t);
|
|
||||||
theme = t;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize theme from storage and apply
|
// Initialize theme from storage and apply
|
||||||
@@ -37,17 +32,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<Header {theme} {setTheme} />
|
<Header theme={$themeStore} setTheme={setTheme} />
|
||||||
|
|
||||||
<main class="game-selection">
|
<main class="game-selection">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -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) {
|
export function clearSessionState(key) {
|
||||||
localStorage.removeItem(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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