mirror of
https://github.com/shadoll/sLogos.git
synced 2025-12-20 04:27:59 +00:00
Add WelcomeStats component and integrate session management in FlagQuiz
- Created a new WelcomeStats component to display user statistics and session results. - Integrated session management in FlagQuiz, allowing users to track their performance across multiple sessions. - Updated game state handling to include session completion and results display. - Enhanced user experience with improved navigation and session reset functionality.
This commit is contained in:
7
public/icons/chart-square.svg
Normal file
7
public/icons/chart-square.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 18V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 18V6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M17 18V13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C21.5093 4.43821 21.8356 5.80655 21.9449 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 837 B |
7
public/icons/chart.svg
Normal file
7
public/icons/chart.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 22H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 17C3 17.9428 3 18.4142 3.29289 18.7071C3.58579 19 4.05719 19 5 19C5.94281 19 6.41421 19 6.70711 18.7071C7 18.4142 7 17.9428 7 17V11C7 10.0572 7 9.58579 6.70711 9.29289C6.41421 9 5.94281 9 5 9C4.05719 9 3.58579 9 3.29289 9.29289C3 9.58579 3 10.0572 3 11V13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M10 7C10 6.05719 10 5.58579 10.2929 5.29289C10.5858 5 11.0572 5 12 5C12.9428 5 13.4142 5 13.7071 5.29289C14 5.58579 14 6.05719 14 7V17C14 17.9428 14 18.4142 13.7071 18.7071C13.4142 19 12.9428 19 12 19C11.0572 19 10.5858 19 10.2929 18.7071C10 18.4142 10 17.9428 10 17V7Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M21 11V17C21 17.9428 21 18.4142 20.7071 18.7071C20.4142 19 19.9428 19 19 19C18.0572 19 17.5858 19 17.2929 18.7071C17 18.4142 17 17.9428 17 17V4C17 3.05719 17 2.58579 17.2929 2.29289C17.5858 2 18.0572 2 19 2C19.9428 2 20.4142 2 20.7071 2.29289C21 2.58579 21 3.05719 21 4V7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 8V6C17 4.11438 17 3.17157 16.4142 2.58579C15.8284 2 14.8856 2 13 2H11C9.11438 2 8.17157 2 7.58579 2.58579C7 3.17157 7 4.11438 7 6V8" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M19.7943 11.0312C19.7943 9.93319 19.1944 8.92292 18.2305 8.39728L13.4362 5.78311C12.541 5.29495 11.4591 5.29495 10.5638 5.78311L5.76962 8.39728C4.80563 8.92292 4.20581 9.93318 4.20581 11.0312V15.9688C4.20581 17.0668 4.80563 18.0771 5.76962 18.6027L10.5638 21.2169C11.4591 21.705 12.541 21.705 13.4362 21.2169L18.2305 18.6027C19.1944 18.0771 19.7943 17.0668 19.7943 15.9688V15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -37,17 +37,40 @@
|
||||
export let collection = "logos";
|
||||
export let setCollection = () => {};
|
||||
// Quiz-only data
|
||||
export let score = null;
|
||||
export let gameStats = null;
|
||||
export let achievementCount = { unlocked: 0, total: 0 };
|
||||
export let onAchievementClick = () => {};
|
||||
export let sessionStats = null; // New prop for session stats
|
||||
export let isQuizActive = false; // New prop to determine if quiz is active
|
||||
|
||||
let dropdownOpen = false;
|
||||
let showAllTimeStats = false; // State to toggle between session and all-time stats
|
||||
let previousQuizState = false; // Track previous quiz state to detect new quiz start
|
||||
|
||||
// Check if we're in game mode
|
||||
$: isGameMode = $location && $location.startsWith('/game');
|
||||
$: isQuizPage = $location && $location.startsWith('/game/flags');
|
||||
|
||||
// Determine default stats view based on quiz state
|
||||
$: {
|
||||
// Detect when quiz becomes active (new quiz started)
|
||||
if (isQuizActive && !previousQuizState) {
|
||||
// New quiz started - show session stats by default
|
||||
showAllTimeStats = false;
|
||||
} else if (!isQuizActive) {
|
||||
// Quiz not active - show all-time stats
|
||||
showAllTimeStats = true;
|
||||
}
|
||||
// Update previous state
|
||||
previousQuizState = isQuizActive;
|
||||
}
|
||||
|
||||
function toggleStatsView() {
|
||||
if (isQuizActive) {
|
||||
showAllTimeStats = !showAllTimeStats;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTitleClick() {
|
||||
dropdownOpen = !dropdownOpen;
|
||||
}
|
||||
@@ -123,13 +146,27 @@
|
||||
<div class="header-row header-controls" class:quiz-mode={isQuizPage}>
|
||||
{#if isQuizPage}
|
||||
<div class="quiz-header-stats">
|
||||
<div class="qh-block">
|
||||
<span class="qh-label">Current:</span>
|
||||
<span class="qh-value">{score ? `${score.correct}/${score.total}` : '0/0'} {score && score.skipped > 0 ? `(` : ''}{#if score && score.skipped > 0}<span class="quiz-icon quiz-skip"><InlineSvg path="/icons/skip-square.svg" alt="Skipped" /></span> {score.skipped}){/if}</span>
|
||||
</div>
|
||||
<div class="qh-block">
|
||||
<span class="qh-label">All Time:</span>
|
||||
<span class="qh-value"><span class="quiz-icon quiz-ok"><InlineSvg path="/icons/ok-square.svg" alt="Correct" /></span> {gameStats?.correct || 0} <span class="quiz-icon quiz-fail"><InlineSvg path="/icons/fail-square.svg" alt="Wrong" /></span> {gameStats?.wrong || 0} {gameStats?.skipped > 0 ? `` : ''}{#if gameStats?.skipped > 0}<span class="quiz-icon quiz-skip"><InlineSvg path="/icons/skip-square.svg" alt="Skipped" /></span> {gameStats.skipped}{/if}</span>
|
||||
<div class="qh-block stats-block">
|
||||
{#if isQuizActive && !showAllTimeStats}
|
||||
<!-- Session Stats -->
|
||||
<span class="qh-value">
|
||||
<span class="quiz-icon quiz-ok"><InlineSvg path="/icons/ok-square.svg" alt="Correct" /></span> {sessionStats?.correct || 0}
|
||||
<span class="quiz-icon quiz-fail"><InlineSvg path="/icons/fail-square.svg" alt="Wrong" /></span> {sessionStats?.wrong || 0}
|
||||
<span class="quiz-icon quiz-skip"><InlineSvg path="/icons/skip-square.svg" alt="Skipped" /></span> {sessionStats?.skipped || 0}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- All Time Stats -->
|
||||
<span class="qh-value">
|
||||
<span class="quiz-icon quiz-ok"><InlineSvg path="/icons/ok-square.svg" alt="Correct" /></span> {gameStats?.correct || 0}
|
||||
<span class="quiz-icon quiz-fail"><InlineSvg path="/icons/fail-square.svg" alt="Wrong" /></span> {gameStats?.wrong || 0}
|
||||
<span class="quiz-icon quiz-skip"><InlineSvg path="/icons/skip-square.svg" alt="Skipped" /></span> {gameStats?.skipped || 0}
|
||||
</span>
|
||||
{/if}
|
||||
{#if isQuizActive}
|
||||
<button class="stats-toggle-btn" class:active={showAllTimeStats} on:click={toggleStatsView} title={showAllTimeStats ? "Show session stats" : "Show all-time stats"}>
|
||||
<InlineSvg path="/icons/chart-square.svg" alt="Toggle stats" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="qh-block achievement-block">
|
||||
<AchievementButton
|
||||
@@ -371,14 +408,52 @@
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.qh-label {
|
||||
color: var(--color-text-secondary);
|
||||
margin-right: 0.35rem;
|
||||
|
||||
.qh-block.stats-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.qh-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.stats-toggle-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.stats-toggle-btn.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.stats-toggle-btn.active:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.stats-toggle-btn :global(.svg-wrapper) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.quiz-icon {
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
export let focusWrongAnswers = false;
|
||||
export let reduceCorrectAnswers = false;
|
||||
export let soundEnabled = true;
|
||||
export let sessionLength = 10;
|
||||
export let showSettings = false;
|
||||
export let showResetConfirmation = false;
|
||||
export let quizType = 'flag'; // 'flag', 'logo', 'emblem', etc.
|
||||
|
||||
// Customizable labels for different quiz types
|
||||
export let focusWrongLabel = 'Focus on previously answered incorrectly items';
|
||||
@@ -28,6 +28,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleOverlayClick(event) {
|
||||
// Only close if clicking on the overlay itself, not the modal content
|
||||
if (event.target === event.currentTarget) {
|
||||
toggleSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function resetAllStats() {
|
||||
showResetConfirmation = true;
|
||||
dispatch('resetConfirmation', true);
|
||||
@@ -43,13 +50,14 @@
|
||||
autoAdvance,
|
||||
focusWrongAnswers,
|
||||
reduceCorrectAnswers,
|
||||
soundEnabled
|
||||
soundEnabled,
|
||||
sessionLength
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showSettings}
|
||||
<div class="settings-overlay" on:click={toggleSettings} on:keydown={handleOverlayKeydown} role="button" tabindex="0">
|
||||
<div class="settings-modal" role="dialog" aria-modal="true" on:click|stopPropagation>
|
||||
<div class="settings-overlay" on:click={handleOverlayClick} on:keydown={handleOverlayKeydown} role="button" tabindex="0" aria-label="Close settings">
|
||||
<div class="settings-modal" role="dialog" aria-modal="true">
|
||||
<div class="settings-header">
|
||||
<InlineSvg path="/icons/settings.svg" alt="Settings" />
|
||||
<h3>Game Settings</h3>
|
||||
@@ -99,6 +107,20 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label for="sessionLength">
|
||||
Quiz length (number of questions):
|
||||
<select id="sessionLength" bind:value={sessionLength}>
|
||||
<option value={5}>5 questions</option>
|
||||
<option value={10}>10 questions</option>
|
||||
<option value={15}>15 questions</option>
|
||||
<option value={20}>20 questions</option>
|
||||
<option value={25}>25 questions</option>
|
||||
<option value={50}>50 questions</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<button class="reset-stats-btn" on:click={resetAllStats}>
|
||||
Reset All Statistics
|
||||
@@ -120,7 +142,7 @@
|
||||
<p>This action will permanently delete:</p>
|
||||
<ul>
|
||||
<li>✗ All game statistics (correct, wrong, skipped answers)</li>
|
||||
<li>✗ Current session score</li>
|
||||
<li>✗ Current quiz score</li>
|
||||
<li>✗ All unlocked achievements</li>
|
||||
<li>✗ Achievement progress</li>
|
||||
<li>✗ Wrong answer tracking data</li>
|
||||
@@ -241,12 +263,55 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.setting-item label[for="sessionLength"] {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-item select {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 1rem;
|
||||
padding-right: 2.5rem;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.setting-item select:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-bg-hover, var(--color-bg-secondary));
|
||||
}
|
||||
|
||||
.setting-item select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(70, 25, 194, 0.1);
|
||||
}
|
||||
|
||||
.setting-item select option {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-actions {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
414
src/components/SessionResults.svelte
Normal file
414
src/components/SessionResults.svelte
Normal file
@@ -0,0 +1,414 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import InlineSvg from './InlineSvg.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show = false;
|
||||
export let sessionStats = {
|
||||
correct: 0,
|
||||
wrong: 0,
|
||||
skipped: 0,
|
||||
total: 0,
|
||||
sessionLength: 10
|
||||
};
|
||||
|
||||
function playAgain() {
|
||||
dispatch('playAgain');
|
||||
}
|
||||
|
||||
function goToGames() {
|
||||
dispatch('goToGames');
|
||||
}
|
||||
|
||||
function closeResults() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
$: percentage = sessionStats.total > 0 ? Math.round((sessionStats.correct / sessionStats.total) * 100) : 0;
|
||||
$: grade = getGrade(percentage);
|
||||
|
||||
function getGrade(percentage) {
|
||||
if (percentage >= 90) return { letter: 'A+', color: '#22c55e', description: 'Excellent!' };
|
||||
if (percentage >= 80) return { letter: 'A', color: '#22c55e', description: 'Great job!' };
|
||||
if (percentage >= 70) return { letter: 'B', color: '#3b82f6', description: 'Good work!' };
|
||||
if (percentage >= 60) return { letter: 'C', color: '#f59e0b', description: 'Not bad!' };
|
||||
if (percentage >= 50) return { letter: 'D', color: '#ef4444', description: 'Keep practicing!' };
|
||||
return { letter: 'F', color: '#ef4444', description: 'Try again!' };
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="results-overlay" role="dialog" aria-modal="true">
|
||||
<div class="results-modal">
|
||||
<div class="results-header">
|
||||
<div class="header-content">
|
||||
<div class="trophy-icon">
|
||||
<InlineSvg path="/icons/cup.svg" alt="Quiz Complete" />
|
||||
</div>
|
||||
<h2>Quiz Complete!</h2>
|
||||
</div>
|
||||
<button class="close-btn" on:click={closeResults} aria-label="Close results">
|
||||
<span class="close-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="results-content">
|
||||
<div class="grade-display">
|
||||
<div class="grade-circle" style="border-color: {grade.color}; color: {grade.color}">
|
||||
{grade.letter}
|
||||
</div>
|
||||
<div class="grade-text">
|
||||
<div class="percentage">{percentage}%</div>
|
||||
<div class="description">{grade.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item correct">
|
||||
<div class="stat-icon">
|
||||
<InlineSvg path="/icons/check-square.svg" alt="Correct" />
|
||||
</div>
|
||||
<div class="stat-value">{sessionStats.correct}</div>
|
||||
<div class="stat-label">Correct</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item wrong">
|
||||
<div class="stat-icon">
|
||||
<InlineSvg path="/icons/close-square.svg" alt="Wrong" />
|
||||
</div>
|
||||
<div class="stat-value">{sessionStats.wrong}</div>
|
||||
<div class="stat-label">Wrong</div>
|
||||
</div>
|
||||
|
||||
{#if sessionStats.skipped > 0}
|
||||
<div class="stat-item skipped">
|
||||
<div class="stat-icon">
|
||||
<InlineSvg path="/icons/unread.svg" alt="Skipped" />
|
||||
</div>
|
||||
<div class="stat-value">{sessionStats.skipped}</div>
|
||||
<div class="stat-label">Skipped</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {percentage}%; background-color: {grade.color}"></div>
|
||||
</div>
|
||||
|
||||
<div class="summary-text">
|
||||
You answered {sessionStats.correct} out of {sessionStats.total} questions correctly
|
||||
{#if sessionStats.skipped > 0}
|
||||
and skipped {sessionStats.skipped} question{sessionStats.skipped > 1 ? 's' : ''}
|
||||
{/if}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-actions">
|
||||
<button class="btn btn-secondary" on:click={goToGames}>
|
||||
Back to Games
|
||||
</button>
|
||||
<button class="btn btn-primary" on:click={playAgain}>
|
||||
Play Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.results-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.results-modal {
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.trophy-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.trophy-icon :global(.svg-wrapper) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.results-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ff5f57;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-icon::before {
|
||||
content: '×';
|
||||
color: #8b0000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-btn:hover .close-icon {
|
||||
background: #ff3b30;
|
||||
}
|
||||
|
||||
.close-btn:hover .close-icon::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.results-content {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.grade-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grade-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 4px solid;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.grade-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
.stat-item.correct .stat-icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.stat-item.wrong .stat-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-item.skipped .stat-icon {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.8s ease-out;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark, #0056b3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grade-display {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grade-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
531
src/components/WelcomeStats.svelte
Normal file
531
src/components/WelcomeStats.svelte
Normal file
@@ -0,0 +1,531 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import InlineSvg from './InlineSvg.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
|
||||
export let sessionStats = null;
|
||||
export let showSessionResults = false;
|
||||
export let sessionLength = 10;
|
||||
|
||||
function startQuiz() {
|
||||
dispatch('startQuiz');
|
||||
}
|
||||
|
||||
function handlePlayAgain() {
|
||||
dispatch('startQuiz');
|
||||
}
|
||||
|
||||
function handleGoToGames() {
|
||||
window.location.hash = '#/game';
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
dispatch('openSettings');
|
||||
}
|
||||
|
||||
function handleCloseResults() {
|
||||
dispatch('closeResults');
|
||||
}
|
||||
|
||||
$: hasPlayedBefore = gameStats.total > 0;
|
||||
$: totalQuestions = gameStats.correct + gameStats.wrong + gameStats.skipped;
|
||||
$: accuracy = totalQuestions > 0 ? Math.round((gameStats.correct / totalQuestions) * 100) : 0;
|
||||
|
||||
// Session results calculations
|
||||
$: sessionPercentage = sessionStats && sessionStats.total > 0 ? Math.round((sessionStats.correct / sessionStats.total) * 100) : 0;
|
||||
$: sessionGrade = sessionStats ? getGrade(sessionPercentage) : null;
|
||||
|
||||
// Welcome page grade display for accuracy
|
||||
$: accuracyGrade = hasPlayedBefore ? getGrade(accuracy) : null;
|
||||
|
||||
function getGrade(percentage) {
|
||||
if (percentage >= 90) return { letter: 'A+', color: '#22c55e', description: 'Excellent!' };
|
||||
if (percentage >= 80) return { letter: 'A', color: '#22c55e', description: 'Great job!' };
|
||||
if (percentage >= 70) return { letter: 'B', color: '#3b82f6', description: 'Good work!' };
|
||||
if (percentage >= 60) return { letter: 'C', color: '#f59e0b', description: 'Not bad!' };
|
||||
if (percentage >= 50) return { letter: 'D', color: '#ef4444', description: 'Keep practicing!' };
|
||||
return { letter: 'F', color: '#ef4444', description: 'Try again!' };
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<div class="stat-icon correct">
|
||||
<InlineSvg path="/icons/check-square.svg" alt="Correct" />
|
||||
</div>
|
||||
<div class="stat-value">{sessionStats.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>
|
||||
<div class="stat-value">{sessionStats.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>
|
||||
<div class="stat-value">{sessionStats.skipped}</div>
|
||||
<div class="stat-label">Skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {sessionPercentage}%; background-color: {sessionGrade.color}"></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-summary">
|
||||
<h3>You answered {sessionStats.correct} out of {sessionStats.total} questions correctly
|
||||
{#if sessionStats.skipped > 0}
|
||||
and skipped {sessionStats.skipped} question{sessionStats.skipped > 1 ? 's' : ''}
|
||||
{/if}.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn secondary" on:click={handleGoToGames}>
|
||||
Back to Games
|
||||
</button>
|
||||
<button class="action-btn primary" on:click={handlePlayAgain}>
|
||||
Play Again
|
||||
</button>
|
||||
<button class="action-btn secondary" on:click={openSettings}>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Welcome/Stats View -->
|
||||
<div class="welcome-header">
|
||||
<div class="welcome-icon">
|
||||
<InlineSvg path="/icons/flag.svg" alt="Flag Quiz" />
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<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-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="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="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="features">
|
||||
<div class="feature">
|
||||
<InlineSvg path="/icons/global.svg" alt="Global" />
|
||||
<span>Flags from every continent</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="start-section">
|
||||
<button class="start-quiz-btn" on:click={startQuiz}>
|
||||
{hasPlayedBefore ? 'Start New Quiz' : 'Start Quiz'}
|
||||
</button>
|
||||
<p class="session-info">{sessionLength} questions per quiz</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.welcome-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 1.5rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.welcome-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-section h2 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto 0.75rem;
|
||||
}
|
||||
|
||||
.stat-icon.correct {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.stat-icon.wrong {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-icon.skipped {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.progress-summary {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.progress-summary h3 {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.grade-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grade-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 4px solid;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.accuracy-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.accuracy-icon :global(.svg-wrapper) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.grade-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.8s ease-out;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: var(--color-primary-dark, #0056b3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-message h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.welcome-message p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.feature :global(.svg-wrapper) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.start-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.start-quiz-btn {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.start-quiz-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.session-info {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.welcome-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.welcome-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.features {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grade-display {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grade-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
import InlineSvg from '../components/InlineSvg.svelte';
|
||||
import Achievements from '../components/Achievements.svelte';
|
||||
import QuizSettings from '../components/QuizSettings.svelte';
|
||||
import WelcomeStats from '../components/WelcomeStats.svelte';
|
||||
|
||||
// Game data
|
||||
let flags = [];
|
||||
@@ -17,7 +18,8 @@
|
||||
let correctAnswer = '';
|
||||
|
||||
// Game states
|
||||
let gameState = 'loading'; // 'loading', 'question', 'answered', 'finished'
|
||||
let gameState = 'welcome'; // 'welcome', 'loading', 'question', 'answered', 'session-complete'
|
||||
let quizSubpage = 'welcome'; // 'welcome' or 'quiz'
|
||||
let selectedAnswer = null;
|
||||
let answered = false;
|
||||
let isAnswered = false;
|
||||
@@ -56,6 +58,15 @@
|
||||
let focusWrongAnswers = false;
|
||||
let reduceCorrectAnswers = false;
|
||||
let soundEnabled = true;
|
||||
let sessionLength = 10;
|
||||
|
||||
// Session management
|
||||
let currentSessionQuestions = 0;
|
||||
let sessionStats = { correct: 0, wrong: 0, skipped: 0, total: 0, sessionLength: 10 };
|
||||
let showSessionResults = false;
|
||||
let sessionInProgress = false;
|
||||
let sessionStartTime = null;
|
||||
let sessionRestoredFromReload = false; // Track if session was restored from page reload
|
||||
|
||||
// Theme
|
||||
let theme = 'system';
|
||||
@@ -73,7 +84,7 @@
|
||||
|
||||
// Save settings when they change (after initial load)
|
||||
$: if (settingsLoaded && typeof reduceCorrectAnswers !== 'undefined') {
|
||||
localStorage.setItem('flagQuizSettings', JSON.stringify({ autoAdvance, focusWrongAnswers, reduceCorrectAnswers, soundEnabled }));
|
||||
localStorage.setItem('flagQuizSettings', JSON.stringify({ autoAdvance, focusWrongAnswers, reduceCorrectAnswers, soundEnabled, sessionLength }));
|
||||
}
|
||||
|
||||
// Load game stats from localStorage
|
||||
@@ -140,6 +151,7 @@
|
||||
focusWrongAnswers = settings.focusWrongAnswers !== undefined ? settings.focusWrongAnswers : false;
|
||||
reduceCorrectAnswers = settings.reduceCorrectAnswers !== undefined ? settings.reduceCorrectAnswers : false;
|
||||
soundEnabled = settings.soundEnabled !== undefined ? settings.soundEnabled : true;
|
||||
sessionLength = settings.sessionLength !== undefined ? settings.sessionLength : 10;
|
||||
} catch (e) {
|
||||
console.error('Error loading settings:', e);
|
||||
}
|
||||
@@ -148,7 +160,9 @@
|
||||
|
||||
await loadFlags();
|
||||
settingsLoaded = true;
|
||||
generateQuestion();
|
||||
|
||||
// Load or initialize session
|
||||
loadSessionState();
|
||||
});
|
||||
|
||||
function applyTheme(theme) {
|
||||
@@ -192,6 +206,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
function saveSessionState() {
|
||||
const sessionState = {
|
||||
sessionInProgress,
|
||||
currentSessionQuestions,
|
||||
sessionStats,
|
||||
score,
|
||||
currentQuestion,
|
||||
selectedAnswer,
|
||||
showResult,
|
||||
gameState,
|
||||
quizSubpage,
|
||||
sessionStartTime,
|
||||
questionKey
|
||||
};
|
||||
localStorage.setItem('flagQuizSessionState', JSON.stringify(sessionState));
|
||||
}
|
||||
|
||||
function loadSessionState() {
|
||||
const savedState = localStorage.getItem('flagQuizSessionState');
|
||||
if (savedState) {
|
||||
try {
|
||||
const state = JSON.parse(savedState);
|
||||
if (state.sessionInProgress) {
|
||||
// Restore session
|
||||
sessionInProgress = state.sessionInProgress;
|
||||
currentSessionQuestions = state.currentSessionQuestions || 0;
|
||||
sessionStats = state.sessionStats || { correct: 0, wrong: 0, skipped: 0, total: 0, sessionLength };
|
||||
score = state.score || { correct: 0, total: 0, skipped: 0 };
|
||||
currentQuestion = state.currentQuestion;
|
||||
selectedAnswer = state.selectedAnswer;
|
||||
showResult = state.showResult || false;
|
||||
gameState = state.gameState || 'question';
|
||||
quizSubpage = 'quiz';
|
||||
sessionStartTime = state.sessionStartTime;
|
||||
questionKey = state.questionKey || 0;
|
||||
|
||||
// Mark that session was restored from reload
|
||||
sessionRestoredFromReload = true;
|
||||
|
||||
// If we don't have a current question, generate one
|
||||
if (!currentQuestion) {
|
||||
generateQuestion();
|
||||
}
|
||||
} else {
|
||||
// No active session, show welcome page
|
||||
quizSubpage = 'welcome';
|
||||
gameState = 'welcome';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading session state:', e);
|
||||
quizSubpage = 'welcome';
|
||||
gameState = 'welcome';
|
||||
}
|
||||
} else {
|
||||
// No saved state, show welcome page
|
||||
quizSubpage = 'welcome';
|
||||
gameState = 'welcome';
|
||||
}
|
||||
}
|
||||
|
||||
function clearSessionState() {
|
||||
localStorage.removeItem('flagQuizSessionState');
|
||||
}
|
||||
|
||||
function generateQuestion() {
|
||||
if (flags.length < 4) {
|
||||
console.error('Not enough flags to generate question');
|
||||
@@ -306,6 +384,9 @@
|
||||
};
|
||||
|
||||
console.log('Generated question:', currentQuestion);
|
||||
|
||||
// Save session state
|
||||
saveSessionState();
|
||||
} function selectAnswer(index) {
|
||||
if (gameState !== 'question') return;
|
||||
|
||||
@@ -317,10 +398,14 @@
|
||||
|
||||
// Update score
|
||||
score.total++;
|
||||
currentSessionQuestions++;
|
||||
sessionStats.total++;
|
||||
|
||||
const isCorrect = index === correctAnswer;
|
||||
if (isCorrect) {
|
||||
score.correct++;
|
||||
gameStats.correct++;
|
||||
sessionStats.correct++;
|
||||
currentStreak++;
|
||||
|
||||
// Play correct sound
|
||||
@@ -350,6 +435,7 @@
|
||||
}
|
||||
} else {
|
||||
gameStats.wrong++;
|
||||
sessionStats.wrong++;
|
||||
currentStreak = 0; // Reset streak on wrong answer
|
||||
|
||||
// Play wrong sound
|
||||
@@ -377,6 +463,27 @@
|
||||
achievementsComponent.checkAchievements();
|
||||
}
|
||||
|
||||
// Save session state
|
||||
saveSessionState();
|
||||
|
||||
// Check if session is complete
|
||||
if (currentSessionQuestions >= sessionLength) {
|
||||
// Session complete - show results and return to welcome page
|
||||
gameState = 'session-complete';
|
||||
sessionStats.sessionLength = sessionLength;
|
||||
|
||||
if (autoAdvance) {
|
||||
const delay = isCorrect ? 2000 : 4000;
|
||||
setTimeout(() => {
|
||||
endSession();
|
||||
}, delay);
|
||||
} else {
|
||||
// Show session complete state immediately
|
||||
endSession();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-advance to next question with different delays if auto mode is on
|
||||
if (autoAdvance) {
|
||||
const delay = isCorrect ? 2000 : 4000; // Double delay for wrong answers
|
||||
@@ -389,6 +496,9 @@
|
||||
score.skipped++;
|
||||
gameStats.skipped++;
|
||||
gameStats.total++;
|
||||
currentSessionQuestions++;
|
||||
sessionStats.skipped++;
|
||||
sessionStats.total++;
|
||||
|
||||
// Track consecutive skips for Speed Runner achievement
|
||||
if (achievementsComponent) {
|
||||
@@ -403,6 +513,17 @@
|
||||
// Save stats to localStorage
|
||||
localStorage.setItem('flagQuizStats', JSON.stringify(gameStats));
|
||||
|
||||
// Save session state
|
||||
saveSessionState();
|
||||
|
||||
// Check if session is complete
|
||||
if (currentSessionQuestions >= sessionLength) {
|
||||
gameState = 'session-complete';
|
||||
sessionStats.sessionLength = sessionLength;
|
||||
endSession();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to next question immediately
|
||||
generateQuestion();
|
||||
}
|
||||
@@ -439,11 +560,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetGame() {
|
||||
function startNewSession() {
|
||||
// Reset session data
|
||||
score = { correct: 0, total: 0, skipped: 0 };
|
||||
currentSessionQuestions = 0;
|
||||
sessionStats = { correct: 0, wrong: 0, skipped: 0, total: 0, sessionLength };
|
||||
sessionInProgress = true;
|
||||
sessionStartTime = Date.now();
|
||||
showSessionResults = false;
|
||||
sessionRestoredFromReload = false; // Reset reload flag for new sessions
|
||||
|
||||
// Switch to quiz subpage
|
||||
quizSubpage = 'quiz';
|
||||
gameState = 'loading';
|
||||
|
||||
// Generate first question
|
||||
generateQuestion();
|
||||
}
|
||||
|
||||
function endSession() {
|
||||
// Clear session state
|
||||
sessionInProgress = false;
|
||||
clearSessionState();
|
||||
|
||||
// Switch to welcome/stats page
|
||||
quizSubpage = 'welcome';
|
||||
gameState = 'welcome';
|
||||
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));
|
||||
@@ -459,11 +608,15 @@
|
||||
}
|
||||
|
||||
function handleSettingsChange(event) {
|
||||
const { autoAdvance: newAutoAdvance, focusWrongAnswers: newFocusWrong, reduceCorrectAnswers: newReduceCorrect, soundEnabled: newSoundEnabled } = event.detail;
|
||||
const { autoAdvance: newAutoAdvance, focusWrongAnswers: newFocusWrong, reduceCorrectAnswers: newReduceCorrect, soundEnabled: newSoundEnabled, sessionLength: newSessionLength } = event.detail;
|
||||
autoAdvance = newAutoAdvance;
|
||||
focusWrongAnswers = newFocusWrong;
|
||||
reduceCorrectAnswers = newReduceCorrect;
|
||||
soundEnabled = newSoundEnabled;
|
||||
sessionLength = newSessionLength;
|
||||
|
||||
// Update current session stats with new session length
|
||||
sessionStats.sessionLength = newSessionLength;
|
||||
}
|
||||
|
||||
function handleSettingsToggle(event) {
|
||||
@@ -479,6 +632,8 @@
|
||||
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
|
||||
@@ -497,11 +652,24 @@
|
||||
showResetConfirmation = false;
|
||||
}
|
||||
|
||||
function handleSessionPlayAgain() {
|
||||
resetGame();
|
||||
}
|
||||
|
||||
function handleSessionGoToGames() {
|
||||
window.location.hash = '#/game';
|
||||
}
|
||||
|
||||
function handleSessionClose() {
|
||||
showSessionResults = false;
|
||||
}
|
||||
|
||||
function cancelReset() {
|
||||
showResetConfirmation = false;
|
||||
}
|
||||
|
||||
function nextQuestion() {
|
||||
sessionRestoredFromReload = false; // Clear reload flag when user manually continues
|
||||
generateQuestion();
|
||||
}
|
||||
|
||||
@@ -583,169 +751,184 @@
|
||||
<Header
|
||||
{theme}
|
||||
{setTheme}
|
||||
{score}
|
||||
{gameStats}
|
||||
{achievementCount}
|
||||
sessionStats={sessionStats}
|
||||
isQuizActive={sessionInProgress && quizSubpage === 'quiz'}
|
||||
onAchievementClick={() => showAchievements = true}
|
||||
/>
|
||||
|
||||
<main class="flag-quiz">
|
||||
<div class="container">
|
||||
<!-- Quiz Settings Component -->
|
||||
<QuizSettings
|
||||
bind:autoAdvance
|
||||
bind:focusWrongAnswers
|
||||
bind:reduceCorrectAnswers
|
||||
bind:soundEnabled
|
||||
bind:sessionLength
|
||||
bind:showSettings
|
||||
bind:showResetConfirmation
|
||||
focusWrongLabel="Focus on previously answered incorrectly flags"
|
||||
reduceCorrectLabel="Show correctly answered flags less frequently"
|
||||
on:settingsChange={handleSettingsChange}
|
||||
on:settingsToggle={handleSettingsToggle}
|
||||
on:resetConfirmation={handleResetConfirmation}
|
||||
on:resetStats={handleResetStats}
|
||||
/>
|
||||
|
||||
<!-- Achievements Component -->
|
||||
<Achievements
|
||||
bind:this={achievementsComponent}
|
||||
{gameStats}
|
||||
{currentStreak}
|
||||
show={showAchievements}
|
||||
on:close={() => showAchievements = false}
|
||||
on:achievementsUnlocked={handleAchievementsUnlocked}
|
||||
/>
|
||||
|
||||
<!-- Quiz Settings Component -->
|
||||
<QuizSettings
|
||||
bind:autoAdvance
|
||||
bind:focusWrongAnswers
|
||||
bind:reduceCorrectAnswers
|
||||
bind:soundEnabled
|
||||
bind:showSettings
|
||||
bind:showResetConfirmation
|
||||
focusWrongLabel="Focus on previously answered incorrectly flags"
|
||||
reduceCorrectLabel="Show correctly answered flags less frequently"
|
||||
on:settingsChange={handleSettingsChange}
|
||||
on:settingsToggle={handleSettingsToggle}
|
||||
on:resetConfirmation={handleResetConfirmation}
|
||||
on:resetStats={handleResetStats}
|
||||
/>
|
||||
|
||||
<!-- Achievements Component -->
|
||||
<Achievements
|
||||
bind:this={achievementsComponent}
|
||||
{gameStats}
|
||||
{currentStreak}
|
||||
show={showAchievements}
|
||||
on:close={() => showAchievements = false}
|
||||
on:achievementsUnlocked={handleAchievementsUnlocked}
|
||||
/>
|
||||
{#if gameState === 'loading'}
|
||||
<div class="loading">Loading flags...</div>
|
||||
{:else if currentQuestion}
|
||||
<div class="question-container">
|
||||
<div class="question-header">
|
||||
<div class="question-number">Question {score.total + 1}</div>
|
||||
<div class="question-type">
|
||||
{currentQuestion.type === 'flag-to-country' ? 'Which country does this flag belong to?' : 'Which flag belongs to this country?'}
|
||||
{#if quizSubpage === 'welcome'}
|
||||
<!-- Welcome/Stats Subpage -->
|
||||
<WelcomeStats
|
||||
{gameStats}
|
||||
{sessionStats}
|
||||
{sessionLength}
|
||||
showSessionResults={showSessionResults}
|
||||
on:startQuiz={startNewSession}
|
||||
on:openSettings={() => showSettings = true}
|
||||
on:closeResults={() => showSessionResults = false}
|
||||
/>
|
||||
{:else if quizSubpage === 'quiz'}
|
||||
<!-- Quiz Subpage -->
|
||||
{#if gameState === 'loading'}
|
||||
<div class="loading">Loading flags...</div>
|
||||
{:else if currentQuestion}
|
||||
<div class="question-container">
|
||||
<div class="question-header">
|
||||
<div class="question-number">Question {currentSessionQuestions + 1} from {sessionLength}</div>
|
||||
<div class="question-type">
|
||||
{currentQuestion.type === 'flag-to-country' ? 'Which country does this flag belong to?' : 'Which flag belongs to this country?'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed height result area -->
|
||||
<div class="result-area">
|
||||
{#if showResult}
|
||||
<div class="result">
|
||||
{#if selectedAnswer === correctAnswer}
|
||||
<div class="correct-result"><span class="result-icon smile-icon"><InlineSvg path="/icons/smile-squre.svg" alt="Correct" /></span> Correct!</div>
|
||||
{:else}
|
||||
<div class="wrong-result">
|
||||
<span class="result-icon sad-icon"><InlineSvg path="/icons/sad-square.svg" alt="Wrong" /></span> Wrong!
|
||||
{#if currentQuestion.type === 'flag-to-country'}
|
||||
<span class="result-country-info">
|
||||
The correct answer is: {getCountryName(currentQuestion.correct)}.
|
||||
<button
|
||||
class="info-icon result-info-btn"
|
||||
aria-label="Show country info"
|
||||
aria-expanded={showResultCountryInfo}
|
||||
on:click={() => (showResultCountryInfo = !showResultCountryInfo)}
|
||||
on:keydown={(e) => { if (e.key === 'Escape') showResultCountryInfo = false; }}
|
||||
>
|
||||
<InlineSvg path="/icons/info-square.svg" alt="Country info" />
|
||||
</button>
|
||||
{#if showResultCountryInfo}
|
||||
<div class="info-tooltip result-info-tooltip" role="dialog" aria-live="polite">
|
||||
{currentQuestion.correct.meta.description}
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
You selected the {getCountryName(currentQuestion.options[selectedAnswer])} flag.
|
||||
<!-- Fixed height result area -->
|
||||
<div class="result-area">
|
||||
{#if showResult}
|
||||
<div class="result">
|
||||
{#if selectedAnswer === correctAnswer}
|
||||
<div class="correct-result"><span class="result-icon smile-icon"><InlineSvg path="/icons/smile-squre.svg" alt="Correct" /></span> Correct!</div>
|
||||
{:else}
|
||||
<div class="wrong-result">
|
||||
<span class="result-icon sad-icon"><InlineSvg path="/icons/sad-square.svg" alt="Wrong" /></span> Wrong!
|
||||
{#if currentQuestion.type === 'flag-to-country'}
|
||||
<span class="result-country-info">
|
||||
The correct answer is: {getCountryName(currentQuestion.correct)}.
|
||||
<button
|
||||
class="info-icon result-info-btn"
|
||||
aria-label="Show country info"
|
||||
aria-expanded={showResultCountryInfo}
|
||||
on:click={() => (showResultCountryInfo = !showResultCountryInfo)}
|
||||
on:keydown={(e) => { if (e.key === 'Escape') showResultCountryInfo = false; }}
|
||||
>
|
||||
<InlineSvg path="/icons/info-square.svg" alt="Country info" />
|
||||
</button>
|
||||
{#if showResultCountryInfo}
|
||||
<div class="info-tooltip result-info-tooltip" role="dialog" aria-live="polite">
|
||||
{currentQuestion.correct.meta.description}
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
You selected the {getCountryName(currentQuestion.options[selectedAnswer])} flag.
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if currentQuestion.type === 'flag-to-country'}
|
||||
<div class="flag-display">
|
||||
<img src={getFlagImage(currentQuestion.correct)} alt="Flag" class="quiz-flag" />
|
||||
</div>
|
||||
|
||||
<div class="options" key={questionKey}>
|
||||
{#each currentQuestion.options as option, index}
|
||||
<button
|
||||
class="option"
|
||||
class:selected={selectedAnswer === index}
|
||||
class:correct={showResult && index === correctAnswer}
|
||||
class:wrong={showResult && selectedAnswer === index && index !== correctAnswer}
|
||||
on:click={() => selectAnswer(index)}
|
||||
disabled={gameState === 'answered'}
|
||||
>
|
||||
{getCountryName(option)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="country-display">
|
||||
<h2 class="country-name">
|
||||
{getCountryName(currentQuestion.correct)}
|
||||
{#if currentQuestion.correct?.meta?.description}
|
||||
<button
|
||||
class="info-icon"
|
||||
aria-label="Show country info"
|
||||
aria-expanded={showCountryInfo}
|
||||
on:click={() => (showCountryInfo = !showCountryInfo)}
|
||||
on:keydown={(e) => { if (e.key === 'Escape') showCountryInfo = false; }}
|
||||
>
|
||||
<InlineSvg path="/icons/info-square.svg" alt="Country info" />
|
||||
</button>
|
||||
{#if showCountryInfo}
|
||||
<div class="info-tooltip" role="dialog" aria-live="polite">
|
||||
{currentQuestion.correct.meta.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flag-options" key={questionKey}>
|
||||
{#each currentQuestion.options as option, index}
|
||||
<button
|
||||
class="flag-option"
|
||||
class:selected={selectedAnswer === index}
|
||||
class:correct={showResult && index === correctAnswer}
|
||||
class:wrong={showResult && selectedAnswer === index && index !== correctAnswer}
|
||||
on:click={() => selectAnswer(index)}
|
||||
disabled={gameState === 'answered'}
|
||||
>
|
||||
<img src={getFlagImage(option)} alt={getCountryName(option)} class="option-flag" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if gameState === 'question'}
|
||||
<button class="btn btn-skip btn-next-full" on:click={skipQuestion}>Skip Question</button>
|
||||
{:else if (!autoAdvance && gameState === 'answered') || (autoAdvance && gameState === 'answered' && sessionRestoredFromReload)}
|
||||
<button class="btn btn-primary btn-next-full" on:click={nextQuestion}>Next Question →</button>
|
||||
{/if}
|
||||
|
||||
<!-- Auto-advance timer display -->
|
||||
{#if autoAdvance && gameState === 'answered' && timerProgress > 0 && !sessionRestoredFromReload}
|
||||
<div class="auto-advance-timer">
|
||||
<div class="timer-bar">
|
||||
<div class="timer-progress" style="width: {timerProgress}%"></div>
|
||||
</div>
|
||||
<span class="timer-text">Next question in {Math.ceil((timerDuration - (timerProgress / 100 * timerDuration)) / 1000)}s</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if currentQuestion.type === 'flag-to-country'}
|
||||
<div class="flag-display">
|
||||
<img src={getFlagImage(currentQuestion.correct)} alt="Flag" class="quiz-flag" />
|
||||
</div>
|
||||
|
||||
<div class="options" key={questionKey}>
|
||||
{#each currentQuestion.options as option, index}
|
||||
<button
|
||||
class="option"
|
||||
class:selected={selectedAnswer === index}
|
||||
class:correct={showResult && index === correctAnswer}
|
||||
class:wrong={showResult && selectedAnswer === index && index !== correctAnswer}
|
||||
on:click={() => selectAnswer(index)}
|
||||
disabled={gameState === 'answered'}
|
||||
>
|
||||
{getCountryName(option)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="country-display">
|
||||
<h2 class="country-name">
|
||||
{getCountryName(currentQuestion.correct)}
|
||||
{#if currentQuestion.correct?.meta?.description}
|
||||
<button
|
||||
class="info-icon"
|
||||
aria-label="Show country info"
|
||||
aria-expanded={showCountryInfo}
|
||||
on:click={() => (showCountryInfo = !showCountryInfo)}
|
||||
on:keydown={(e) => { if (e.key === 'Escape') showCountryInfo = false; }}
|
||||
>
|
||||
<InlineSvg path="/icons/info-square.svg" alt="Country info" />
|
||||
</button>
|
||||
{#if showCountryInfo}
|
||||
<div class="info-tooltip" role="dialog" aria-live="polite">
|
||||
{currentQuestion.correct.meta.description}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flag-options" key={questionKey}>
|
||||
{#each currentQuestion.options as option, index}
|
||||
<button
|
||||
class="flag-option"
|
||||
class:selected={selectedAnswer === index}
|
||||
class:correct={showResult && index === correctAnswer}
|
||||
class:wrong={showResult && selectedAnswer === index && index !== correctAnswer}
|
||||
on:click={() => selectAnswer(index)}
|
||||
disabled={gameState === 'answered'}
|
||||
>
|
||||
<img src={getFlagImage(option)} alt={getCountryName(option)} class="option-flag" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if gameState === 'question'}
|
||||
<button class="btn btn-skip btn-next-full" on:click={skipQuestion}>Skip Question</button>
|
||||
{:else if !autoAdvance && gameState === 'answered'}
|
||||
<button class="btn btn-primary btn-next-full" on:click={nextQuestion}>Next Question →</button>
|
||||
{/if}
|
||||
|
||||
<!-- Auto-advance timer display -->
|
||||
{#if autoAdvance && gameState === 'answered' && timerProgress > 0}
|
||||
<div class="auto-advance-timer">
|
||||
<div class="timer-bar">
|
||||
<div class="timer-progress" style="width: {timerProgress}%"></div>
|
||||
</div>
|
||||
<span class="timer-text">Next question in {Math.ceil((timerDuration - (timerProgress / 100 * timerDuration)) / 1000)}s</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="controls">
|
||||
<button class="btn btn-secondary" on:click={endSession}>End Quiz</button>
|
||||
<button class="btn btn-secondary" on:click={toggleSettings} title="Settings">Settings</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="controls">
|
||||
<a href="#/game" class="btn btn-secondary">Back to Games</a>
|
||||
<button class="btn btn-secondary" on:click={resetGame}>New Session</button>
|
||||
<button class="btn btn-secondary" on:click={toggleSettings} title="Settings">Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
@@ -765,205 +948,6 @@
|
||||
|
||||
/* Removed header-top/settings-btn styles; settings now lives in controls */
|
||||
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.settings-header :global(.svg-wrapper) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-primary);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.setting-item label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-item input[type="checkbox"] {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-actions {
|
||||
border-top: 2px solid var(--border-color);
|
||||
padding-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.reset-stats-btn {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-stats-btn:hover {
|
||||
background: #cc3333;
|
||||
}
|
||||
|
||||
/* Confirmation Dialog Styles */
|
||||
.confirmation-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.confirmation-dialog {
|
||||
background: var(--color-bg-primary);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.confirmation-header {
|
||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.confirmation-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.confirmation-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.confirmation-content p {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.confirmation-content ul {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.confirmation-content li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.confirmation-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: var(--color-bg-hover);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: #dc2626;
|
||||
border: 1px solid #dc2626;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
|
||||
Reference in New Issue
Block a user