mirror of
https://github.com/shadoll/sLogos.git
synced 2026-08-28 11:33:29 +00:00
Add Geography Quiz and related components with quiz info and routing
This commit is contained in:
+113
-67
@@ -9,6 +9,7 @@
|
||||
import CapitalsQuiz from "./pages/CapitalsQuiz.svelte";
|
||||
import NotFound from "./pages/NotFound.svelte";
|
||||
import Header from "./components/Header.svelte";
|
||||
import GeographyQuiz from "./pages/GeographyQuiz.svelte";
|
||||
|
||||
export const routes = {
|
||||
"/": Home,
|
||||
@@ -16,6 +17,7 @@
|
||||
"/game": Game,
|
||||
"/game/flags": FlagQuiz,
|
||||
"/game/capitals": CapitalsQuiz,
|
||||
"/game/geography": GeographyQuiz,
|
||||
"*": NotFound,
|
||||
};
|
||||
|
||||
@@ -87,7 +89,11 @@
|
||||
selectedVariants = [];
|
||||
tagDropdownOpen = false;
|
||||
compactMode = false;
|
||||
console.error("loadCollectionData: error loading data for", collection, error);
|
||||
console.error(
|
||||
"loadCollectionData: error loading data for",
|
||||
collection,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,11 +108,16 @@
|
||||
window.appData.filteredLogos = window.appData.logos.filter((logo) => {
|
||||
const matchesSearch =
|
||||
logo.name.toLowerCase().includes(val.toLowerCase()) ||
|
||||
(logo.title && logo.title.toLowerCase().includes(val.toLowerCase())) ||
|
||||
(logo.brand && logo.brand.toLowerCase().includes(val.toLowerCase())) ||
|
||||
(logo.meta && Object.values(logo.meta).some(
|
||||
v => typeof v === 'string' && v.toLowerCase().includes(val.toLowerCase())
|
||||
));
|
||||
(logo.title &&
|
||||
logo.title.toLowerCase().includes(val.toLowerCase())) ||
|
||||
(logo.brand &&
|
||||
logo.brand.toLowerCase().includes(val.toLowerCase())) ||
|
||||
(logo.meta &&
|
||||
Object.values(logo.meta).some(
|
||||
(v) =>
|
||||
typeof v === "string" &&
|
||||
v.toLowerCase().includes(val.toLowerCase()),
|
||||
));
|
||||
const matchesTags =
|
||||
!selectedTags.length ||
|
||||
(logo.tags &&
|
||||
@@ -207,7 +218,7 @@
|
||||
// Restore selected tags from URL
|
||||
const tagsParam = params.get("tags");
|
||||
if (tagsParam) {
|
||||
selectedTags = tagsParam.split(",").filter(tag => tag.trim());
|
||||
selectedTags = tagsParam.split(",").filter((tag) => tag.trim());
|
||||
console.log("App: Restored selectedTags from URL:", selectedTags);
|
||||
// Update localStorage with URL values
|
||||
localStorage.setItem("selectedTags", JSON.stringify(selectedTags));
|
||||
@@ -216,7 +227,7 @@
|
||||
// Restore selected brands from URL
|
||||
const brandsParam = params.get("brands");
|
||||
if (brandsParam) {
|
||||
selectedBrands = brandsParam.split(",").filter(brand => brand.trim());
|
||||
selectedBrands = brandsParam.split(",").filter((brand) => brand.trim());
|
||||
console.log("App: Restored selectedBrands from URL:", selectedBrands);
|
||||
// Update localStorage with URL values localStorage.setItem("selectedBrands", JSON.stringify(selectedBrands));
|
||||
}
|
||||
@@ -224,10 +235,15 @@
|
||||
// Restore selected variants from URL
|
||||
const variantsParam = params.get("variants");
|
||||
if (variantsParam) {
|
||||
selectedVariants = variantsParam.split(",").filter(variant => variant.trim());
|
||||
selectedVariants = variantsParam
|
||||
.split(",")
|
||||
.filter((variant) => variant.trim());
|
||||
console.log("App: Restored selectedVariants from URL:", selectedVariants);
|
||||
// Update localStorage with URL values
|
||||
localStorage.setItem("selectedVariants", JSON.stringify(selectedVariants));
|
||||
localStorage.setItem(
|
||||
"selectedVariants",
|
||||
JSON.stringify(selectedVariants),
|
||||
);
|
||||
}
|
||||
|
||||
// Force update window.appData after restoration
|
||||
@@ -236,7 +252,10 @@
|
||||
window.appData.selectedTags = [...selectedTags];
|
||||
window.appData.selectedBrands = [...selectedBrands];
|
||||
window.appData.selectedVariants = [...selectedVariants];
|
||||
console.log("App: Updated window.appData after restoration with variants:", selectedVariants);
|
||||
console.log(
|
||||
"App: Updated window.appData after restoration with variants:",
|
||||
selectedVariants,
|
||||
);
|
||||
updateFilteredLogosImmediate();
|
||||
|
||||
// Force re-render of components by updating references
|
||||
@@ -246,11 +265,15 @@
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Restore view mode and compact mode from localStorage
|
||||
const savedViewMode = localStorage.getItem("viewMode");
|
||||
if (savedViewMode === "grid" || savedViewMode === "list" || savedViewMode === "compact") {
|
||||
viewMode = savedViewMode;
|
||||
}
|
||||
// Restore view mode and compact mode from localStorage
|
||||
const savedViewMode = localStorage.getItem("viewMode");
|
||||
if (
|
||||
savedViewMode === "grid" ||
|
||||
savedViewMode === "list" ||
|
||||
savedViewMode === "compact"
|
||||
) {
|
||||
viewMode = savedViewMode;
|
||||
}
|
||||
const savedCompact = localStorage.getItem("compactMode");
|
||||
if (savedCompact === "true" || savedCompact === "false") {
|
||||
setCompactMode(savedCompact === "true");
|
||||
@@ -264,7 +287,10 @@
|
||||
const parsedTags = JSON.parse(savedTags);
|
||||
if (Array.isArray(parsedTags)) {
|
||||
selectedTags = parsedTags;
|
||||
console.log("App: Restored selectedTags from localStorage:", selectedTags);
|
||||
console.log(
|
||||
"App: Restored selectedTags from localStorage:",
|
||||
selectedTags,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("App: Error parsing saved tags:", error);
|
||||
@@ -281,7 +307,10 @@
|
||||
const parsedBrands = JSON.parse(savedBrands);
|
||||
if (Array.isArray(parsedBrands)) {
|
||||
selectedBrands = parsedBrands;
|
||||
console.log("App: Restored selectedBrands from localStorage:", selectedBrands);
|
||||
console.log(
|
||||
"App: Restored selectedBrands from localStorage:",
|
||||
selectedBrands,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("App: Error parsing saved brands:", error);
|
||||
@@ -298,7 +327,10 @@
|
||||
const parsedVariants = JSON.parse(savedVariants);
|
||||
if (Array.isArray(parsedVariants)) {
|
||||
selectedVariants = parsedVariants;
|
||||
console.log("App: Restored selectedVariants from localStorage:", selectedVariants);
|
||||
console.log(
|
||||
"App: Restored selectedVariants from localStorage:",
|
||||
selectedVariants,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("App: Error parsing saved variants:", error);
|
||||
@@ -330,11 +362,16 @@
|
||||
$: filteredLogos = logos.filter((logo) => {
|
||||
const matchesSearch =
|
||||
logo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(logo.title && logo.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.brand && logo.brand.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.meta && Object.values(logo.meta).some(
|
||||
v => typeof v === 'string' && v.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
));
|
||||
(logo.title &&
|
||||
logo.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.brand &&
|
||||
logo.brand.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.meta &&
|
||||
Object.values(logo.meta).some(
|
||||
(v) =>
|
||||
typeof v === "string" &&
|
||||
v.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
));
|
||||
const matchesTags =
|
||||
!selectedTags.length ||
|
||||
(logo.tags &&
|
||||
@@ -640,7 +677,10 @@
|
||||
console.log("App: Adding variant:", variant);
|
||||
if (!selectedVariants.includes(variant)) {
|
||||
selectedVariants = [...selectedVariants, variant];
|
||||
localStorage.setItem("selectedVariants", JSON.stringify(selectedVariants));
|
||||
localStorage.setItem(
|
||||
"selectedVariants",
|
||||
JSON.stringify(selectedVariants),
|
||||
);
|
||||
console.log("App: Updated selectedVariants:", selectedVariants);
|
||||
|
||||
// Update window.appData immediately
|
||||
@@ -691,11 +731,16 @@
|
||||
window.appData.filteredLogos = window.appData.logos.filter((logo) => {
|
||||
const matchesSearch =
|
||||
logo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(logo.title && logo.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.brand && logo.brand.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.meta && Object.values(logo.meta).some(
|
||||
v => typeof v === 'string' && v.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
));
|
||||
(logo.title &&
|
||||
logo.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.brand &&
|
||||
logo.brand.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(logo.meta &&
|
||||
Object.values(logo.meta).some(
|
||||
(v) =>
|
||||
typeof v === "string" &&
|
||||
v.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
));
|
||||
const matchesTags =
|
||||
!selectedTags.length ||
|
||||
(logo.tags &&
|
||||
@@ -753,17 +798,19 @@
|
||||
}
|
||||
|
||||
function getTagObj(tag) {
|
||||
const tagObj = allTags.find(t => t.text === tag);
|
||||
const tagObj = allTags.find((t) => t.text === tag);
|
||||
return tagObj || { text: tag };
|
||||
}
|
||||
|
||||
function openLogoByAnchor(hash) {
|
||||
if (!hash || !hash.startsWith('#/preview/')) return;
|
||||
if (!hash || !hash.startsWith("#/preview/")) return;
|
||||
|
||||
const logoName = hash.replace('#/preview/', '').replace(/-/g, ' ');
|
||||
const logo = logos.find(l =>
|
||||
l.name.toLowerCase().replace(/\s+/g, '-') === hash.replace('#/preview/', '').toLowerCase() ||
|
||||
l.name.toLowerCase() === logoName.toLowerCase()
|
||||
const logoName = hash.replace("#/preview/", "").replace(/-/g, " ");
|
||||
const logo = logos.find(
|
||||
(l) =>
|
||||
l.name.toLowerCase().replace(/\s+/g, "-") ===
|
||||
hash.replace("#/preview/", "").toLowerCase() ||
|
||||
l.name.toLowerCase() === logoName.toLowerCase(),
|
||||
);
|
||||
|
||||
if (logo) {
|
||||
@@ -774,16 +821,16 @@
|
||||
|
||||
// Listen for outside click to close dropdown
|
||||
function handleOutsideClick(event) {
|
||||
if (tagDropdownOpen && !event.target.closest('.filter-dropdown')) {
|
||||
if (tagDropdownOpen && !event.target.closest(".filter-dropdown")) {
|
||||
closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
$: if (typeof window !== "undefined") {
|
||||
if (tagDropdownOpen) {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
document.addEventListener("click", handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
document.removeEventListener("click", handleOutsideClick);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -791,37 +838,36 @@
|
||||
<Router {routes} let:Component>
|
||||
<svelte:component
|
||||
this={Component}
|
||||
displayLogos={displayLogos}
|
||||
{displayLogos}
|
||||
allLogos={logos}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
viewMode={viewMode}
|
||||
setGridView={setGridView}
|
||||
setListView={setListView}
|
||||
setCompactView={setCompactView}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
allTags={allTags}
|
||||
selectedTags={selectedTags}
|
||||
selectedBrands={selectedBrands}
|
||||
selectedVariants={selectedVariants}
|
||||
tagDropdownOpen={tagDropdownOpen}
|
||||
toggleDropdown={toggleDropdown}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
addBrand={addBrand}
|
||||
removeBrand={removeBrand}
|
||||
addVariant={addVariant}
|
||||
removeVariant={removeVariant}
|
||||
getTagObj={getTagObj}
|
||||
compactMode={compactMode}
|
||||
setCompactMode={setCompactMode}
|
||||
collection={collection}
|
||||
setCollection={setCollection}
|
||||
collections={collections}
|
||||
{theme}
|
||||
{setTheme}
|
||||
{viewMode}
|
||||
{setGridView}
|
||||
{setListView}
|
||||
{setCompactView}
|
||||
{searchQuery}
|
||||
{setSearchQuery}
|
||||
{allTags}
|
||||
{selectedTags}
|
||||
{selectedBrands}
|
||||
{selectedVariants}
|
||||
{tagDropdownOpen}
|
||||
{toggleDropdown}
|
||||
{addTag}
|
||||
{removeTag}
|
||||
{addBrand}
|
||||
{removeBrand}
|
||||
{addVariant}
|
||||
{removeVariant}
|
||||
{getTagObj}
|
||||
{compactMode}
|
||||
{setCompactMode}
|
||||
{collection}
|
||||
{setCollection}
|
||||
{collections}
|
||||
/>
|
||||
</Router>
|
||||
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
<div class="action-buttons">
|
||||
{#if mode === 'welcome'}
|
||||
<button class="action-btn secondary" on:click={() => handleAction('goToGames')}>
|
||||
Back to Games
|
||||
</button>
|
||||
<button class="action-btn primary" on:click={() => handleAction('startQuiz')}>
|
||||
{hasPlayedBefore ? 'Start New Quiz' : 'Start Quiz'}
|
||||
</button>
|
||||
|
||||
+117
-103
@@ -8,6 +8,7 @@
|
||||
export let sessionStats = null;
|
||||
export let showSessionResults = false;
|
||||
export let sessionLength = 10;
|
||||
export let quizInfo;
|
||||
|
||||
function startQuiz() {
|
||||
dispatch("startQuiz");
|
||||
@@ -72,27 +73,39 @@
|
||||
<div class="welcome-container">
|
||||
{#if showSessionResults && sessionStats}
|
||||
<!-- Session Results View -->
|
||||
<div class="welcome-header">
|
||||
<div class="welcome-icon">
|
||||
<InlineSvg path="/icons/check-circle.svg" alt="Quiz Complete" />
|
||||
</div>
|
||||
<h1>Quiz Complete!</h1>
|
||||
<p class="welcome-subtitle">Great job on completing the quiz</p>
|
||||
<div class="welcome-stats">
|
||||
{#if quizInfo}
|
||||
<div class="quiz-info">
|
||||
{#if quizInfo.icon}
|
||||
<div class="quiz-icon">
|
||||
<img src={`/icons/${quizInfo.icon}.svg`} alt="Quiz icon" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if quizInfo.title}
|
||||
<div class="quiz-title">{quizInfo.title}</div>
|
||||
{/if}
|
||||
{#if quizInfo.description}
|
||||
<div class="quiz-description">{quizInfo.description}</div>
|
||||
{/if}
|
||||
{#if quizInfo.features}
|
||||
<ul class="quiz-features">
|
||||
{#each quizInfo.features as feature}
|
||||
<li class="feature">
|
||||
{#if feature.icon}
|
||||
<InlineSvg path={feature.icon} alt={feature.text} />
|
||||
{/if}
|
||||
<span>{feature.text}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grade-display">
|
||||
<div class="percentage">{sessionPercentage}%</div>
|
||||
<div class="description">{sessionGrade.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<div class="grade-display">
|
||||
<div
|
||||
class="grade-circle"
|
||||
style="border-color: {sessionGrade.color}; color: {sessionGrade.color}"
|
||||
>
|
||||
{sessionGrade.letter}
|
||||
</div>
|
||||
<div class="grade-text">
|
||||
<div class="percentage">{sessionPercentage}%</div>
|
||||
<div class="description">{sessionGrade.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
@@ -145,106 +158,107 @@
|
||||
{/if}.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of session results block -->
|
||||
{:else}
|
||||
<!-- Welcome/Stats View -->
|
||||
<div class="welcome-header">
|
||||
<div class="welcome-icon">
|
||||
<InlineSvg path="/icons/flag.svg" alt="Flag Quiz" />
|
||||
{#if quizInfo}
|
||||
<div class="welcome-header">
|
||||
{#if quizInfo.icon}
|
||||
<div class="welcome-icon">
|
||||
<InlineSvg path={quizInfo.icon} alt={quizInfo.title} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if quizInfo.title}
|
||||
<h1>{quizInfo.title}</h1>
|
||||
{/if}
|
||||
{#if quizInfo.description}
|
||||
<p class="welcome-subtitle">{quizInfo.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<h1>Flag Quiz</h1>
|
||||
<p class="welcome-subtitle">Test your knowledge of world flags</p>
|
||||
</div>
|
||||
|
||||
{#if hasPlayedBefore}
|
||||
<div class="stats-section">
|
||||
<h2>Your Statistics</h2>
|
||||
{#if hasPlayedBefore}
|
||||
<div class="stats-section">
|
||||
<h2>Your Statistics</h2>
|
||||
|
||||
<div class="grade-display">
|
||||
<div
|
||||
class="accuracy-icon"
|
||||
style="color: {accuracyGrade.color}"
|
||||
>
|
||||
<InlineSvg
|
||||
path="/icons/medal-star.svg"
|
||||
alt="Accuracy"
|
||||
/>
|
||||
</div>
|
||||
<div class="grade-text">
|
||||
<div class="percentage">{accuracy}%</div>
|
||||
<div class="description">Accuracy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon correct">
|
||||
<div class="grade-display">
|
||||
<div
|
||||
class="accuracy-icon"
|
||||
style="color: {accuracyGrade?.color}"
|
||||
>
|
||||
<InlineSvg
|
||||
path="/icons/check-square.svg"
|
||||
alt="Correct"
|
||||
path="/icons/medal-star.svg"
|
||||
alt="Accuracy"
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.correct}</div>
|
||||
<div class="stat-label">Correct</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon wrong">
|
||||
<InlineSvg
|
||||
path="/icons/close-square.svg"
|
||||
alt="Wrong"
|
||||
/>
|
||||
<div class="grade-text">
|
||||
<div class="percentage">{accuracy}%</div>
|
||||
<div class="description">Accuracy</div>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.wrong}</div>
|
||||
<div class="stat-label">Wrong</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon skipped">
|
||||
<InlineSvg
|
||||
path="/icons/skip-square.svg"
|
||||
alt="Skipped"
|
||||
/>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon correct">
|
||||
<InlineSvg
|
||||
path="/icons/check-square.svg"
|
||||
alt="Correct"
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.correct}</div>
|
||||
<div class="stat-label">Correct</div>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.skipped}</div>
|
||||
<div class="stat-label">Skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-summary">
|
||||
<h3>Total Questions Answered: {totalQuestions}</h3>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="welcome-message">
|
||||
<h2>Welcome to Flag Quiz!</h2>
|
||||
<p>
|
||||
Challenge yourself to identify flags from around the world.
|
||||
Each quiz contains <strong>{sessionLength} questions</strong
|
||||
> with a mix of flag-to-country and country-to-flag challenges.
|
||||
</p>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon wrong">
|
||||
<InlineSvg
|
||||
path="/icons/close-square.svg"
|
||||
alt="Wrong"
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.wrong}</div>
|
||||
<div class="stat-label">Wrong</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<InlineSvg path="/icons/global.svg" alt="Global" />
|
||||
<span>Flags from every continent</span>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon skipped">
|
||||
<InlineSvg
|
||||
path="/icons/skip-square.svg"
|
||||
alt="Skipped"
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-value">{gameStats.skipped}</div>
|
||||
<div class="stat-label">Skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<InlineSvg
|
||||
path="/icons/medal-ribbon.svg"
|
||||
alt="Achievements"
|
||||
/>
|
||||
<span>Unlock achievements</span>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<InlineSvg
|
||||
path="/icons/chart-square.svg"
|
||||
alt="Statistics"
|
||||
/>
|
||||
<span>Track your progress</span>
|
||||
|
||||
<div class="progress-summary">
|
||||
<h3>Total Questions Answered: {totalQuestions}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="welcome-message">
|
||||
{#if quizInfo.title}
|
||||
<h2>Welcome to {quizInfo.title}!</h2>
|
||||
{/if}
|
||||
{#if quizInfo.description}
|
||||
<p>{quizInfo.description}</p>
|
||||
{/if}
|
||||
{#if quizInfo.features}
|
||||
<div class="features">
|
||||
{#each quizInfo.features as feature}
|
||||
{#if feature.icon && feature.text}
|
||||
<div class="feature">
|
||||
<InlineSvg path={feature.icon} alt={feature.text} />
|
||||
<span>{feature.text}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="feature">{feature}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { quizInfo } from '../quizInfo/CapitalsQuizInfo.js';
|
||||
import { onMount } from "svelte";
|
||||
import Header from "../components/Header.svelte";
|
||||
import Footer from "../components/Footer.svelte";
|
||||
@@ -984,6 +985,7 @@
|
||||
{sessionStats}
|
||||
{sessionLength}
|
||||
{showSessionResults}
|
||||
quizInfo={quizInfo}
|
||||
on:startQuiz={startNewSession}
|
||||
on:openSettings={() => (showSettings = true)}
|
||||
on:closeResults={() => (showSessionResults = false)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { quizInfo } from '../quizInfo/FlagQuizInfo.js';
|
||||
import { onMount } from "svelte";
|
||||
import Header from "../components/Header.svelte";
|
||||
import Footer from "../components/Footer.svelte";
|
||||
@@ -969,6 +970,7 @@
|
||||
{sessionStats}
|
||||
{sessionLength}
|
||||
{showSessionResults}
|
||||
quizInfo={quizInfo}
|
||||
on:startQuiz={startNewSession}
|
||||
on:openSettings={() => (showSettings = true)}
|
||||
on:closeResults={() => (showSessionResults = false)}
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
description: 'Test your knowledge of world capitals',
|
||||
icon: '🏛️',
|
||||
route: '#/game/capitals'
|
||||
},
|
||||
{
|
||||
name: 'geography',
|
||||
title: 'Geography Quiz',
|
||||
description: 'Test your knowledge of world geography',
|
||||
icon: '🗺️',
|
||||
route: '#/game/geography'
|
||||
}
|
||||
// Future games will be added here
|
||||
];
|
||||
|
||||
let theme = 'system';
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
|
||||
<script>
|
||||
import { quizInfo } from '../quizInfo/GeographyQuizInfo.js';
|
||||
import { onMount } from "svelte";
|
||||
import Header from "../components/Header.svelte";
|
||||
import Footer from "../components/Footer.svelte";
|
||||
import CountryMap from "../components/CountryMap.svelte";
|
||||
import WelcomeStats from "../components/WelcomeStats.svelte";
|
||||
import ActionButtons from "../components/ActionButtons.svelte";
|
||||
|
||||
// Game data
|
||||
let countries = [];
|
||||
let currentQuestion = null;
|
||||
let correctAnswer = null;
|
||||
let options = [];
|
||||
|
||||
// Game states
|
||||
let gameState = "welcome"; // 'welcome', 'loading', 'question', 'answered', 'session-complete'
|
||||
let quizSubpage = "welcome";
|
||||
let selectedAnswer = null;
|
||||
let showResult = false;
|
||||
let questionKey = 0;
|
||||
let sessionLength = 10;
|
||||
let currentSessionQuestions = 0;
|
||||
let sessionStats = {
|
||||
correct: 0,
|
||||
wrong: 0,
|
||||
total: 0,
|
||||
sessionLength: 10,
|
||||
};
|
||||
let showSessionResults = false;
|
||||
let sessionInProgress = false;
|
||||
|
||||
async function loadCountries() {
|
||||
const res = await fetch("/data/flags.json");
|
||||
const data = await res.json();
|
||||
countries = data.filter(
|
||||
(c) => c.tags && c.tags.includes("Country") && c.code && c.meta?.country
|
||||
);
|
||||
}
|
||||
|
||||
function generateQuestion() {
|
||||
// Pick correct country
|
||||
const idx = Math.floor(Math.random() * countries.length);
|
||||
correctAnswer = countries[idx];
|
||||
// Pick 3 random other countries
|
||||
let other = countries.filter((c) => c.code !== correctAnswer.code);
|
||||
other = shuffle(other).slice(0, 3);
|
||||
options = shuffle([correctAnswer, ...other]);
|
||||
selectedAnswer = null;
|
||||
showResult = false;
|
||||
questionKey++;
|
||||
gameState = "question";
|
||||
quizSubpage = "quiz";
|
||||
currentSessionQuestions++;
|
||||
}
|
||||
|
||||
function shuffle(arr) {
|
||||
return arr
|
||||
.map((v) => [Math.random(), v])
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map((v) => v[1]);
|
||||
}
|
||||
|
||||
function selectAnswer(option) {
|
||||
selectedAnswer = option;
|
||||
showResult = true;
|
||||
gameState = "answered";
|
||||
if (option === correctAnswer) {
|
||||
sessionStats.correct++;
|
||||
} else {
|
||||
sessionStats.wrong++;
|
||||
}
|
||||
sessionStats.total++;
|
||||
}
|
||||
|
||||
function startNewSession() {
|
||||
sessionInProgress = true;
|
||||
quizSubpage = "quiz";
|
||||
gameState = "loading";
|
||||
currentSessionQuestions = 0;
|
||||
sessionStats = {
|
||||
correct: 0,
|
||||
wrong: 0,
|
||||
total: 0,
|
||||
sessionLength,
|
||||
};
|
||||
generateQuestion();
|
||||
}
|
||||
|
||||
function endSession() {
|
||||
sessionInProgress = false;
|
||||
quizSubpage = "welcome";
|
||||
gameState = "welcome";
|
||||
showSessionResults = true;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadCountries();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header />
|
||||
<main class="map-quiz">
|
||||
{#if quizSubpage === "welcome"}
|
||||
<div class="container">
|
||||
<WelcomeStats
|
||||
gameStats={sessionStats}
|
||||
{sessionStats}
|
||||
{sessionLength}
|
||||
{showSessionResults}
|
||||
quizInfo={quizInfo}
|
||||
on:startQuiz={startNewSession}
|
||||
on:openSettings={() => {}}
|
||||
on:closeResults={() => {}}
|
||||
/>
|
||||
<ActionButtons
|
||||
mode={showSessionResults ? "results" : "welcome"}
|
||||
sessionInfo={showSessionResults ? "" : `${sessionLength} questions per quiz`}
|
||||
hasPlayedBefore={sessionStats.total > 0}
|
||||
on:action={startNewSession}
|
||||
/>
|
||||
</div>
|
||||
{:else if quizSubpage === "quiz"}
|
||||
<div class="container">
|
||||
<h2>Question {currentSessionQuestions} of {sessionLength}</h2>
|
||||
{#if correctAnswer}
|
||||
<CountryMap
|
||||
countryCodes={[correctAnswer.code]}
|
||||
countryNames={[]}
|
||||
mapPath="/data/world.svg"
|
||||
countryScale={true}
|
||||
scalePadding={60}
|
||||
/>
|
||||
<div class="options">
|
||||
{#each options as option}
|
||||
<button
|
||||
class="option-btn {selectedAnswer === option ? (option === correctAnswer ? 'correct' : 'wrong') : ''}"
|
||||
on:click={() => selectAnswer(option)}
|
||||
disabled={showResult}
|
||||
>
|
||||
{option.meta.country}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showResult}
|
||||
<div class="result">
|
||||
{selectedAnswer === correctAnswer
|
||||
? 'Correct!'
|
||||
: `Wrong! The correct answer is ${correctAnswer.meta.country}`}
|
||||
</div>
|
||||
{#if currentSessionQuestions < sessionLength}
|
||||
<button class="next-btn" on:click={generateQuestion}>Next</button>
|
||||
{:else}
|
||||
<button class="next-btn" on:click={endSession}>Finish</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<div>Loading...</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
/* Use the same container and layout as FlagQuiz/CapitalsQuiz for welcome page */
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
export const quizInfo = {
|
||||
title: "Capitals Quiz",
|
||||
icon: "/icons/buildings.svg",
|
||||
description: "Test your knowledge of world capitals.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Capitals from every continent" },
|
||||
{ icon: "/icons/medal-ribbon.svg", text: "Perfect rounds and achievements" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export const quizInfo = {
|
||||
title: "Capitals Quiz",
|
||||
icon: "/icons/buildings.svg",
|
||||
description: "Test your knowledge of world capitals.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Capitals from every continent" },
|
||||
{ icon: "/icons/medal-ribbon.svg", text: "Perfect rounds and achievements" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
export const quizInfo = {
|
||||
title: "Flag Quiz",
|
||||
icon: "/icons/flag.svg",
|
||||
description: "Test your knowledge of world flags.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Flags from every continent" },
|
||||
{ icon: "/icons/medal-ribbon.svg", text: "Unlock achievements" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export const quizInfo = {
|
||||
title: "Flag Quiz",
|
||||
icon: "/icons/flag.svg",
|
||||
description: "Test your knowledge of world flags.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Flags from every continent" },
|
||||
{ icon: "/icons/medal-ribbon.svg", text: "Unlock achievements" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
export const quizInfo = {
|
||||
title: "Geography Quiz",
|
||||
icon: "/icons/map.svg",
|
||||
description: "Test your knowledge of world geography by identifying countries from their map shapes.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Countries from every continent" },
|
||||
{ icon: "/icons/map.svg", text: "Unique map-based questions" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export const quizInfo = {
|
||||
title: "Geography Quiz",
|
||||
icon: "/icons/map.svg",
|
||||
description: "Test your knowledge of world geography by identifying countries from their map shapes.",
|
||||
features: [
|
||||
{ icon: "/icons/global.svg", text: "Countries from every continent" },
|
||||
{ icon: "/icons/map.svg", text: "Unique map-based questions" },
|
||||
{ icon: "/icons/chart-square.svg", text: "Track your progress" }
|
||||
]
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user