mirror of
https://github.com/shadoll/sLogos.git
synced 2025-12-20 02:26:05 +00:00
Add Geography Quiz and related components with quiz info and routing
This commit is contained in:
5
public/icons/map.svg
Normal file
5
public/icons/map.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 7.16229C21 6.11871 21 5.59692 20.7169 5.20409C20.4337 4.81126 19.9387 4.64625 18.9487 4.31624L17.7839 3.92799C16.4168 3.47229 15.7333 3.24444 15.0451 3.3366C14.3569 3.42876 13.7574 3.82843 12.5583 4.62778L11.176 5.54937C10.2399 6.1734 9.77191 6.48541 9.24685 6.60952C9.05401 6.65511 8.85714 6.68147 8.6591 6.68823C8.11989 6.70665 7.58626 6.52877 6.51901 6.17302C5.12109 5.70705 4.42213 5.47406 3.89029 5.71066C3.70147 5.79466 3.53204 5.91678 3.39264 6.06935C3 6.49907 3 7.23584 3 8.70938V12.7736M21 11V15.2907C21 16.7642 21 17.501 20.6074 17.9307C20.468 18.0833 20.2985 18.2054 20.1097 18.2894C19.5779 18.526 18.8789 18.293 17.481 17.827C16.4137 17.4713 15.8801 17.2934 15.3409 17.3118C15.1429 17.3186 14.946 17.3449 14.7532 17.3905C14.2281 17.5146 13.7601 17.8266 12.824 18.4507L11.4417 19.3722C10.2426 20.1716 9.64311 20.5713 8.95493 20.6634C8.26674 20.7556 7.58319 20.5277 6.21609 20.072L5.05132 19.6838C4.06129 19.3538 3.56627 19.1888 3.28314 18.7959C3.01507 18.424 3.0008 17.9365 3.00004 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M15 3.5V7M15 17V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M9 20.5V17M9 7V13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
180
src/App.svelte
180
src/App.svelte
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
173
src/pages/GeographyQuiz.svelte
Normal file
173
src/pages/GeographyQuiz.svelte
Normal file
@@ -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>
|
||||
10
src/quizInfo/CapitalsQuizInfo.js
Normal file
10
src/quizInfo/CapitalsQuizInfo.js
Normal file
@@ -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" }
|
||||
]
|
||||
};
|
||||
12
src/quizInfo/CapitalsQuizInfo.svelte
Normal file
12
src/quizInfo/CapitalsQuizInfo.svelte
Normal file
@@ -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>
|
||||
10
src/quizInfo/FlagQuizInfo.js
Normal file
10
src/quizInfo/FlagQuizInfo.js
Normal file
@@ -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" }
|
||||
]
|
||||
};
|
||||
12
src/quizInfo/FlagQuizInfo.svelte
Normal file
12
src/quizInfo/FlagQuizInfo.svelte
Normal file
@@ -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>
|
||||
10
src/quizInfo/GeographyQuizInfo.js
Normal file
10
src/quizInfo/GeographyQuizInfo.js
Normal file
@@ -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" }
|
||||
]
|
||||
};
|
||||
12
src/quizInfo/GeographyQuizInfo.svelte
Normal file
12
src/quizInfo/GeographyQuizInfo.svelte
Normal file
@@ -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