From 6d854eb4a7954132200ceff20cbfe1e10e1599c1 Mon Sep 17 00:00:00 2001 From: pengniklas Date: Fri, 22 May 2026 16:22:50 +0200 Subject: [PATCH 1/4] fix: remove dead main.js, fix lobby footer link, clean up game.js --- frontend/lobby.html | 2 +- frontend/scripts/game.js | 1 - frontend/scripts/main.js | 36 ------------------------------------ 3 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 frontend/scripts/main.js diff --git a/frontend/lobby.html b/frontend/lobby.html index bf8b16d..23143da 100644 --- a/frontend/lobby.html +++ b/frontend/lobby.html @@ -99,7 +99,7 @@ diff --git a/frontend/scripts/game.js b/frontend/scripts/game.js index 24a1a9f..5438b48 100644 --- a/frontend/scripts/game.js +++ b/frontend/scripts/game.js @@ -17,7 +17,6 @@ const elTimerNum = document.getElementById("timer-num"); const elTimerBar = document.getElementById("timer-bar"); const elBtnClear = document.getElementById("btn-clear"); const elBtnSubmit = document.getElementById("btn-submit"); -const elCanvas = document.getElementById("draw-canvas"); // ── Init async function initGame() { diff --git a/frontend/scripts/main.js b/frontend/scripts/main.js deleted file mode 100644 index ebbc12f..0000000 --- a/frontend/scripts/main.js +++ /dev/null @@ -1,36 +0,0 @@ -// main.js — index.html - -document.addEventListener("DOMContentLoaded", () => { - // Smooth scroll for nav links - document.querySelectorAll('a[href^="#"]').forEach(link => { - link.addEventListener("click", e => { - const id = link.getAttribute("href").slice(1); - const target = document.getElementById(id); - if (!target) return; - e.preventDefault(); - const headerH = document.querySelector(".header")?.offsetHeight || 0; - const top = target.getBoundingClientRect().top + window.scrollY - headerH - 16; - window.scrollTo({ top, behavior: "smooth" }); - }); - }); - - // Register button stub - const regBtn = document.getElementById("reg-btn"); - if (regBtn) { - regBtn.addEventListener("click", () => { - alert("Frontend skeleton only. Backend will be connected later."); - }); - } - - // Scroll reveal - const reveals = document.querySelectorAll(".reveal"); - const io = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add("visible"); - io.unobserve(entry.target); - } - }); - }, { threshold: 0.12 }); - reveals.forEach(el => io.observe(el)); -}); From aa1865d734da4a5d091c715b409ad0da20268e7b Mon Sep 17 00:00:00 2001 From: pengniklas Date: Fri, 22 May 2026 16:31:31 +0200 Subject: [PATCH 2/4] merge index.css into main.css, unify form styles --- countries.json | 92 --- frontend/index.html | 4 +- frontend/leaderboard.html | 66 +- frontend/results.html | 260 +------- frontend/scripts/leaderboard.js | 69 ++ frontend/scripts/results.js | 65 ++ frontend/styles/index.css | 1108 +++++++------------------------ frontend/styles/leader.css | 323 +++++---- frontend/styles/lobby.css | 44 +- frontend/styles/main.css | 44 ++ frontend/styles/results.css | 196 ++++++ 11 files changed, 784 insertions(+), 1487 deletions(-) delete mode 100644 countries.json create mode 100644 frontend/scripts/leaderboard.js create mode 100644 frontend/scripts/results.js create mode 100644 frontend/styles/results.css diff --git a/countries.json b/countries.json deleted file mode 100644 index cfdc609..0000000 --- a/countries.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "name": "Switzerland", - "hint": "Alpine country in Central Europe", - "cities": [ - { "name": "Bern", "x": 48, "y": 58 }, - { "name": "Zürich", "x": 58, "y": 38 }, - { "name": "Geneva", "x": 22, "y": 72 } - ] - }, - { - "name": "Norway", - "hint": "Scandinavian country with long coastline", - "cities": [ - { "name": "Oslo", "x": 55, "y": 72 }, - { "name": "Bergen", "x": 32, "y": 60 }, - { "name": "Tromsø", "x": 62, "y": 18 } - ] - }, - { - "name": "Italy", - "hint": "Boot-shaped peninsula in Southern Europe", - "cities": [ - { "name": "Rome", "x": 52, "y": 58 }, - { "name": "Milan", "x": 42, "y": 22 }, - { "name": "Naples", "x": 58, "y": 72 } - ] - }, - { - "name": "Japan", - "hint": "Island nation in East Asia", - "cities": [ - { "name": "Tokyo", "x": 72, "y": 48 }, - { "name": "Osaka", "x": 58, "y": 58 }, - { "name": "Sapporo", "x": 70, "y": 22 } - ] - }, - { - "name": "Brazil", - "hint": "Largest country in South America", - "cities": [ - { "name": "Brasília", "x": 58, "y": 52 }, - { "name": "São Paulo", "x": 60, "y": 68 }, - { "name": "Manaus", "x": 38, "y": 38 } - ] - }, - { - "name": "Australia", - "hint": "Continent and country in the Southern Hemisphere", - "cities": [ - { "name": "Canberra", "x": 72, "y": 72 }, - { "name": "Sydney", "x": 78, "y": 68 }, - { "name": "Perth", "x": 22, "y": 65 } - ] - }, - { - "name": "France", - "hint": "Country in Western Europe, hexagonal shape", - "cities": [ - { "name": "Paris", "x": 50, "y": 32 }, - { "name": "Lyon", "x": 58, "y": 55 }, - { "name": "Marseille", "x": 58, "y": 72 } - ] - }, - { - "name": "India", - "hint": "Large peninsula in South Asia", - "cities": [ - { "name": "New Delhi", "x": 46, "y": 28 }, - { "name": "Mumbai", "x": 32, "y": 55 }, - { "name": "Chennai", "x": 52, "y": 72 } - ] - }, - { - "name": "Canada", - "hint": "Second largest country in the world", - "cities": [ - { "name": "Ottawa", "x": 62, "y": 52 }, - { "name": "Vancouver", "x": 22, "y": 55 }, - { "name": "Toronto", "x": 60, "y": 58 } - ] - }, - { - "name": "Germany", - "hint": "Central European country", - "cities": [ - { "name": "Berlin", "x": 58, "y": 28 }, - { "name": "Munich", "x": 48, "y": 68 }, - { "name": "Hamburg", "x": 42, "y": 18 } - ] - } -] diff --git a/frontend/index.html b/frontend/index.html index 0ac38a3..def43f1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,9 +4,7 @@ GeoDraw - - - + diff --git a/frontend/leaderboard.html b/frontend/leaderboard.html index 637b4ad..8897e84 100644 --- a/frontend/leaderboard.html +++ b/frontend/leaderboard.html @@ -6,7 +6,7 @@ GeoDraw — Leaderboard - +
@@ -87,68 +87,6 @@
- + diff --git a/frontend/results.html b/frontend/results.html index 0c47178..408cbcf 100644 --- a/frontend/results.html +++ b/frontend/results.html @@ -5,200 +5,7 @@ GeoDraw — Results - +
@@ -288,69 +95,6 @@ - + diff --git a/frontend/scripts/leaderboard.js b/frontend/scripts/leaderboard.js new file mode 100644 index 0000000..967da1d --- /dev/null +++ b/frontend/scripts/leaderboard.js @@ -0,0 +1,69 @@ +// leaderboard.js — leaderboard page logic + +document.addEventListener("DOMContentLoaded", () => { + const currentPlayer = Storage.getPlayerName(); + const body = document.getElementById("lb-body"); + + /** + * Escape HTML special characters to prevent XSS. + * @param {string} str - Raw user-provided string. + * @returns {string} HTML-safe string. + */ + function escHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">"); + } + + /** Render the leaderboard from localStorage data. */ + function render() { + const board = Storage.getLeaderboard(); + body.innerHTML = ""; + + if (!board.length) { + body.innerHTML = ` +
+
🏁
+

No games played yet. Be the first on the board!

+ Play now → +
`; + return; + } + + const medals = ["🥇", "🥈", "🥉"]; + const scoreClasses = ["gold", "silver", "bronze"]; + + board.forEach((entry, index) => { + const rank = index + 1; + const isYou = entry.name === currentPlayer; + const date = entry.date + ? new Date(entry.date).toLocaleDateString("en-CH", { day: "2-digit", month: "short" }) + : "—"; + const rounds = (entry.scores || []).join(" · ") || "—"; + + const row = document.createElement("div"); + row.className = `lb-row${rank <= 3 ? ` rank-${rank}` : ""}`; + row.style.animationDelay = `${index * 0.05}s`; + + row.innerHTML = ` + ${rank <= 3 ? `${medals[rank - 1]}` : rank} + ${escHtml(entry.name)} + ${escHtml(rounds)} + ${date} + ${entry.totalScore} + `; + body.appendChild(row); + }); + + // Show the current player's latest score bar + const latest = board.find((entry) => entry.name === currentPlayer); + if (latest) { + document.getElementById("your-bar").style.display = "flex"; + document.getElementById("your-bar-name").textContent = latest.name; + document.getElementById("your-bar-score").textContent = latest.totalScore; + } + } + + render(); +}); diff --git a/frontend/scripts/results.js b/frontend/scripts/results.js new file mode 100644 index 0000000..7efad71 --- /dev/null +++ b/frontend/scripts/results.js @@ -0,0 +1,65 @@ +// results.js — results page logic + +document.addEventListener("DOMContentLoaded", () => { + const state = Storage.getGameState(); + const name = Storage.getPlayerName(); + + document.getElementById("results-player").textContent = name; + + if (!state) { + document.getElementById("results-title").textContent = "No game found."; + document.getElementById("total-score").textContent = "—"; + return; + } + + const { scores, totalScore, countries } = state; + + // Determine emoji and title based on average score + const avg = totalScore / 3; + let emoji = "🌍"; + let title = "Not bad!"; + if (avg >= 85) { emoji = "🔥"; title = "Incredible!"; } + else if (avg >= 70) { emoji = "🎉"; title = "Well done!"; } + else if (avg >= 50) { emoji = "👏"; title = "Good effort!"; } + else { emoji = "😅"; title = "Keep practising!"; } + + document.getElementById("results-emoji").textContent = emoji; + document.getElementById("results-title").textContent = title; + document.getElementById("total-score").textContent = totalScore; + + // Grade badge + const grade = Scoring.getGrade(avg); + const gradeEl = document.getElementById("total-grade"); + gradeEl.textContent = `Grade ${grade.label}`; + + // Round breakdown rows + const rowsContainer = document.getElementById("round-rows"); + (scores || []).forEach((score, index) => { + const row = document.createElement("div"); + row.className = "round-row"; + row.innerHTML = ` + Round ${index + 1} +
+
+
+ ${score}% + `; + rowsContainer.appendChild(row); + }); + + // Animate score bars on next frame so CSS transition fires + requestAnimationFrame(() => { + document.querySelectorAll(".round-row__bar").forEach((bar) => { + bar.style.width = `${bar.dataset.target}%`; + }); + }); + + // Country tags + const tagsContainer = document.getElementById("countries-row"); + (countries || []).forEach((country) => { + const tag = document.createElement("span"); + tag.className = "country-tag"; + tag.textContent = country; + tagsContainer.appendChild(tag); + }); +}); diff --git a/frontend/styles/index.css b/frontend/styles/index.css index 0bff113..533d11d 100644 --- a/frontend/styles/index.css +++ b/frontend/styles/index.css @@ -1,974 +1,354 @@ -:root { - --ink: #0b1f2a; - --ink-soft: #3d5563; - --ink-muted: #7a9aaa; - --sea: #1a7fc4; - --sea-light: #4faae0; - --sea-dim: rgba(26, 127, 196, 0.12); - --leaf: #41b869; - --leaf-dim: rgba(65, 184, 105, 0.13); - --cream: #f4f9f6; - --white: #ffffff; - --glass: rgba(255, 255, 255, 0.72); - --line: rgba(11, 31, 42, 0.09); - --shadow: 0 24px 60px rgba(11, 31, 42, 0.1); - --r-xl: 32px; - --r-lg: 22px; - --r-md: 14px; - --hh: 80px; +/* index.css — landing page specific styles */ + +/* ─── EYEBROW ─── */ +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 14px 7px 10px; + border-radius: 999px; + border: 1px solid var(--leaf-dim); + background: rgba(65, 184, 105, 0.08); + color: #289149; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + margin-bottom: 26px; } -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} -html { - scroll-behavior: smooth; +.eyebrow__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--leaf); + animation: pulse 2s ease-in-out infinite; } -body { - font-family: "DM Sans", sans-serif; - color: var(--ink); - background: var(--cream); - overflow-x: hidden; - position: relative; -} - -body::before { - content: ""; - position: fixed; - inset: 0; - background: - radial-gradient( - ellipse 60% 50% at 10% 0%, - rgba(65, 184, 105, 0.12) 0%, - transparent 60% - ), - radial-gradient( - ellipse 50% 60% at 90% 100%, - rgba(26, 127, 196, 0.12) 0%, - transparent 60% - ); - pointer-events: none; - z-index: 0; -} - -body::after { - content: ""; - position: fixed; - inset: 0; - background-image: - linear-gradient(var(--line) 1px, transparent 1px), - linear-gradient(90deg, var(--line) 1px, transparent 1px); - background-size: 60px 60px; - pointer-events: none; - z-index: 0; -} - -a { - color: inherit; - text-decoration: none; -} -img, -svg { - display: block; - max-width: 100%; -} -button, -input { - font: inherit; -} - -.wrap { - position: relative; - z-index: 1; -} - -.container { - width: 100%; - max-width: 1280px; - margin: 0 auto; - padding: 0 28px; -} - -/* ─── HEADER ─── */ -.header { - position: sticky; - top: 0; - z-index: 100; - height: var(--hh); - display: flex; - align-items: center; - backdrop-filter: blur(16px) saturate(1.4); - -webkit-backdrop-filter: blur(16px) saturate(1.4); - background: rgba(244, 249, 246, 0.82); - border-bottom: 1px solid var(--line); -} - -.header__inner { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; -} - -.logo { - display: inline-flex; - align-items: center; - gap: 12px; -} - -.logo__mark { - width: 44px; - height: 44px; - border-radius: 50%; - background: linear-gradient(135deg, var(--sea) 0%, var(--leaf) 100%); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - position: relative; - overflow: hidden; -} - -.logo__mark svg { - width: 22px; - height: 22px; -} - -.logo__text { - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: 1.25rem; - letter-spacing: -0.02em; -} - -.nav { - display: flex; - align-items: center; - gap: 4px; -} - -.nav__link { - padding: 9px 16px; - border-radius: 999px; - font-size: 0.92rem; - font-weight: 500; - color: var(--ink-soft); - transition: - background 0.18s, - color 0.18s; -} - -.nav__link:hover { - background: var(--sea-dim); - color: var(--sea); -} - -.nav__cta { - margin-left: 8px; - padding: 9px 20px; - border-radius: 999px; - font-size: 0.92rem; - font-weight: 600; - color: var(--white); - background: var(--ink); - transition: - opacity 0.18s, - transform 0.18s; -} - -.nav__cta:hover { - opacity: 0.82; - transform: translateY(-1px); +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.4); opacity: 0.7; } } /* ─── HERO ─── */ .hero { - min-height: calc(100vh - var(--hh)); - display: grid; - place-items: center; - padding: 80px 0 60px; + min-height: calc(100vh - var(--hh)); + display: grid; + place-items: center; + padding: 80px 0 60px; } .hero__inner { - display: grid; - grid-template-columns: 1fr minmax(320px, 500px); - gap: 64px; - align-items: center; -} - -.eyebrow { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 7px 14px 7px 10px; - border-radius: 999px; - border: 1px solid var(--leaf-dim); - background: rgba(65, 184, 105, 0.08); - color: #289149; - font-size: 0.82rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - margin-bottom: 26px; -} - -.eyebrow__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--leaf); - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, - 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.4); - opacity: 0.7; - } + display: grid; + grid-template-columns: 1fr minmax(320px, 500px); + gap: 64px; + align-items: center; } .hero__title { - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: clamp(3.6rem, 8vw, 7rem); - line-height: 0.92; - letter-spacing: -0.04em; - margin-bottom: 24px; + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: clamp(3.6rem, 8vw, 7rem); + line-height: 0.92; + letter-spacing: -0.04em; + margin-bottom: 24px; } .hero__title em { - font-style: normal; - background: linear-gradient(135deg, var(--sea), var(--leaf)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + font-style: normal; + background: linear-gradient(135deg, var(--sea), var(--leaf)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .hero__desc { - font-size: 1.08rem; - line-height: 1.75; - color: var(--ink-soft); - max-width: 520px; - margin-bottom: 38px; + font-size: 1.08rem; + line-height: 1.75; + color: var(--ink-soft); + max-width: 520px; + margin-bottom: 38px; } .hero__actions { - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; } -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 16px 30px; - border-radius: var(--r-lg); - font-family: "DM Sans", sans-serif; - font-size: 1rem; - font-weight: 600; - border: none; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s, - opacity 0.2s; -} - -.btn:hover { - transform: translateY(-2px); -} - -.btn--primary { - color: var(--white); - background: linear-gradient( - 135deg, - var(--sea) 0%, - #159fd4 50%, - var(--leaf) 100% - ); - background-size: 200% 200%; - box-shadow: 0 8px 24px rgba(26, 127, 196, 0.3); - animation: gradShift 4s ease infinite; -} - -@keyframes gradShift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.btn--primary:hover { - box-shadow: 0 12px 32px rgba(26, 127, 196, 0.4); -} - -.btn--ghost { - color: var(--ink-soft); - background: transparent; - border: 1.5px solid var(--line); -} - -.btn--ghost:hover { - border-color: var(--ink-muted); - color: var(--ink); -} - -.btn--full { - width: 100%; -} - -/* Globe card */ +/* ─── GLOBE CARD ─── */ .globe-card { - background: var(--glass); - backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: var(--r-xl); - box-shadow: var(--shadow); - padding: 40px; - display: flex; - align-items: center; - justify-content: center; - min-height: 500px; - position: relative; - overflow: hidden; + background: var(--glass); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: var(--r-xl); + box-shadow: var(--shadow); + padding: 40px; + display: flex; + align-items: center; + justify-content: center; + min-height: 500px; + position: relative; + overflow: hidden; } .globe-card::before { - content: ""; - position: absolute; - inset: 0; - border-radius: var(--r-xl); - background: - radial-gradient( - circle at 30% 25%, - rgba(65, 184, 105, 0.18), - transparent 50% - ), - radial-gradient( - circle at 75% 75%, - rgba(26, 127, 196, 0.15), - transparent 50% - ); + content: ""; + position: absolute; + inset: 0; + border-radius: var(--r-xl); + background: + radial-gradient(circle at 30% 25%, rgba(65, 184, 105, 0.18), transparent 50%), + radial-gradient(circle at 75% 75%, rgba(26, 127, 196, 0.15), transparent 50%); } .globe { - position: relative; - z-index: 1; - width: min(100%, 380px); - aspect-ratio: 1; - border-radius: 50%; - background: radial-gradient( - circle at 35% 30%, - #70d0ff 0%, - var(--sea) 30%, - #0d4b80 65%, - #072f52 100% - ); - box-shadow: - inset -20px -20px 40px rgba(0, 0, 0, 0.22), - inset 8px 8px 24px rgba(255, 255, 255, 0.08), - 0 30px 60px rgba(7, 47, 82, 0.28); + position: relative; + z-index: 1; + width: min(100%, 380px); + aspect-ratio: 1; + border-radius: 50%; + background: radial-gradient( + circle at 35% 30%, + #70d0ff 0%, + var(--sea) 30%, + #0d4b80 65%, + #072f52 100% + ); + box-shadow: + inset -20px -20px 40px rgba(0, 0, 0, 0.22), + inset 8px 8px 24px rgba(255, 255, 255, 0.08), + 0 30px 60px rgba(7, 47, 82, 0.28); } .continent { - position: absolute; - background: #52c870; - border-radius: 46% 54% 58% 42%; + position: absolute; + background: #52c870; + border-radius: 46% 54% 58% 42%; } -.c1 { - width: 30%; - height: 22%; - top: 18%; - left: 12%; - transform: rotate(-14deg); -} -.c2 { - width: 18%; - height: 28%; - top: 30%; - left: 46%; - transform: rotate(8deg); - border-radius: 40% 60% 50% 50%; -} -.c3 { - width: 22%; - height: 16%; - top: 56%; - right: 15%; - transform: rotate(22deg); -} -.c4 { - width: 12%; - height: 18%; - top: 68%; - left: 20%; - transform: rotate(-6deg); - border-radius: 50%; -} +.c1 { width: 30%; height: 22%; top: 18%; left: 12%; transform: rotate(-14deg); } +.c2 { width: 18%; height: 28%; top: 30%; left: 46%; transform: rotate(8deg); border-radius: 40% 60% 50% 50%; } +.c3 { width: 22%; height: 16%; top: 56%; right: 15%; transform: rotate(22deg); } +.c4 { width: 12%; height: 18%; top: 68%; left: 20%; transform: rotate(-6deg); border-radius: 50%; } .globe-line { - position: absolute; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 50%; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + position: absolute; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } -.gl1 { - width: 60%; - height: 100%; -} -.gl2 { - width: 88%; - height: 100%; -} -.gl3 { - width: 100%; - height: 55%; - top: 50%; -} -.gl4 { - width: 100%; - height: 80%; - top: 50%; -} +.gl1 { width: 60%; height: 100%; } +.gl2 { width: 88%; height: 100%; } +.gl3 { width: 100%; height: 55%; top: 50%; } +.gl4 { width: 100%; height: 80%; top: 50%; } .globe-badge { - position: absolute; - z-index: 2; - background: var(--white); - border-radius: var(--r-md); - padding: 10px 14px; - box-shadow: 0 8px 24px rgba(11, 31, 42, 0.14); - font-size: 0.8rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 8px; - animation: floatBadge 3s ease-in-out infinite; + position: absolute; + z-index: 2; + background: var(--white); + border-radius: var(--r-md); + padding: 10px 14px; + box-shadow: 0 8px 24px rgba(11, 31, 42, 0.14); + font-size: 0.8rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + animation: floatBadge 3s ease-in-out infinite; } -.gb--score { - top: 14%; - right: 6%; - animation-delay: 0s; -} -.gb--country { - bottom: 14%; - left: 4%; - animation-delay: 1.2s; -} +.gb--score { top: 14%; right: 6%; animation-delay: 0s; } +.gb--country { bottom: 14%; left: 4%; animation-delay: 1.2s; } @keyframes floatBadge { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-8px); - } + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } -.gb__dot { - width: 8px; - height: 8px; - border-radius: 50%; -} - -.gb__dot--green { - background: var(--leaf); -} -.gb__dot--blue { - background: var(--sea); -} +.gb__dot { width: 8px; height: 8px; border-radius: 50%; } +.gb__dot--green { background: var(--leaf); } +.gb__dot--blue { background: var(--sea); } /* ─── ABOUT ─── */ .about { - padding: 100px 0; + padding: 100px 0; } .about__inner { - display: grid; - grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); - gap: 64px; - align-items: center; -} - -.section-label { - display: inline-block; - padding: 6px 14px; - border-radius: 999px; - background: var(--sea-dim); - color: var(--sea); - font-size: 0.82rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - margin-bottom: 20px; -} - -.section-title { - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: clamp(2.4rem, 4.5vw, 4rem); - line-height: 1.04; - letter-spacing: -0.03em; - margin-bottom: 20px; + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + gap: 64px; + align-items: center; } .about__desc { - font-size: 1.05rem; - line-height: 1.75; - color: var(--ink-soft); - margin-bottom: 36px; + font-size: 1.05rem; + line-height: 1.75; + color: var(--ink-soft); + margin-bottom: 36px; } -/* Steps */ +.mini-globe-wrap { + background: var(--glass); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: var(--r-xl); + box-shadow: var(--shadow); + padding: 32px; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + max-width: 340px; + position: relative; + overflow: hidden; +} + +.mini-globe-wrap::before { + content: ""; + position: absolute; + inset: 0; + border-radius: var(--r-xl); + background: radial-gradient(circle at 60% 40%, rgba(26, 127, 196, 0.1), transparent 50%); +} + +/* ─── STEPS ─── */ .steps { - display: grid; - gap: 16px; + display: grid; + gap: 16px; } .step { - display: grid; - grid-template-columns: 64px 1fr; - gap: 20px; - align-items: center; - background: var(--glass); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: var(--r-lg); - padding: 22px 26px; - box-shadow: 0 4px 20px rgba(11, 31, 42, 0.06); - transition: - transform 0.2s, - box-shadow 0.2s; + display: grid; + grid-template-columns: 64px 1fr; + gap: 20px; + align-items: center; + background: var(--glass); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: var(--r-lg); + padding: 22px 26px; + box-shadow: 0 4px 20px rgba(11, 31, 42, 0.06); + transition: transform 0.2s, box-shadow 0.2s; } .step:hover { - transform: translateX(6px); - box-shadow: 0 8px 28px rgba(11, 31, 42, 0.1); + transform: translateX(6px); + box-shadow: 0 8px 28px rgba(11, 31, 42, 0.1); } .step__num { - width: 56px; - height: 56px; - border-radius: 16px; - background: linear-gradient(135deg, var(--sea), var(--leaf)); - color: var(--white); - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: 1rem; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; + width: 56px; + height: 56px; + border-radius: 16px; + background: linear-gradient(135deg, var(--sea), var(--leaf)); + color: var(--white); + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } .step__title { - font-family: "Syne", sans-serif; - font-weight: 700; - font-size: 1rem; - margin-bottom: 4px; + font-family: "Syne", sans-serif; + font-weight: 700; + font-size: 1rem; + margin-bottom: 4px; } .step__text { - font-size: 0.9rem; - color: var(--ink-soft); - line-height: 1.5; + font-size: 0.9rem; + color: var(--ink-soft); + line-height: 1.5; } /* ─── REGISTER ─── */ .register { - padding: 100px 0; + padding: 100px 0; } .register__inner { - display: grid; - grid-template-columns: 1fr minmax(320px, 560px); - gap: 64px; - align-items: center; + display: grid; + grid-template-columns: 1fr minmax(320px, 560px); + gap: 64px; + align-items: center; } .register__desc { - font-size: 1.05rem; - line-height: 1.75; - color: var(--ink-soft); - margin-top: 16px; + font-size: 1.05rem; + line-height: 1.75; + color: var(--ink-soft); + margin-top: 16px; } .register__card { - background: var(--white); - border: 1px solid var(--line); - border-radius: var(--r-xl); - padding: 44px; - box-shadow: var(--shadow); + background: var(--white); + border: 1px solid var(--line); + border-radius: var(--r-xl); + padding: 44px; + box-shadow: var(--shadow); } .card-title { - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: 1.5rem; - letter-spacing: -0.02em; - margin-bottom: 30px; -} - -.form { - display: grid; - gap: 20px; -} - -.field { - display: grid; - gap: 8px; -} - -.field label { - font-size: 0.86rem; - font-weight: 600; - color: var(--ink-soft); - letter-spacing: 0.02em; -} - -.field input { - width: 100%; - height: 54px; - padding: 0 18px; - border: 1.5px solid var(--line); - border-radius: var(--r-md); - background: var(--cream); - color: var(--ink); - font-size: 0.98rem; - transition: - border-color 0.18s, - box-shadow 0.18s; -} - -.field input::placeholder { - color: var(--ink-muted); -} - -.field input:focus { - outline: none; - border-color: var(--sea-light); - box-shadow: 0 0 0 3px rgba(26, 127, 196, 0.12); - background: var(--white); + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 1.5rem; + letter-spacing: -0.02em; + margin-bottom: 30px; } .form-footer { - margin-top: 8px; - font-size: 0.84rem; - color: var(--ink-muted); - text-align: center; -} - -.mini-globe-wrap { - background: var(--glass); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: var(--r-xl); - box-shadow: var(--shadow); - padding: 32px; - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 1; - max-width: 340px; - position: relative; - overflow: hidden; -} - -.mini-globe-wrap::before { - content: ""; - position: absolute; - inset: 0; - border-radius: var(--r-xl); - background: radial-gradient( - circle at 60% 40%, - rgba(26, 127, 196, 0.1), - transparent 50% - ); -} - -/* ─── FOOTER ─── */ -.footer { - border-top: 1px solid var(--line); - background: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(12px); - padding: 40px 0; -} - -.footer__inner { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 24px; -} - -.footer__brand { - font-family: "Syne", sans-serif; - font-weight: 800; - font-size: 1.5rem; - letter-spacing: -0.02em; -} - -.footer__sub { - font-size: 0.86rem; - color: var(--ink-muted); - margin-top: 4px; -} - -.footer__center { - text-align: center; -} - - -.footer__label { - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); - margin-bottom: 10px; -} - -.footer__right { - justify-self: end; - display: flex; - gap: 22px; - flex-wrap: wrap; - justify-content: flex-end; -} - -.footer__link { - font-size: 0.9rem; - color: var(--ink-muted); - transition: color 0.18s; -} - -.footer__link:hover { - color: var(--ink); + margin-top: 8px; + font-size: 0.84rem; + color: var(--ink-muted); + text-align: center; } /* ─── RESPONSIVE ─── */ @media (max-width: 960px) { - .hero__inner, - .about__inner, - .register__inner { - grid-template-columns: 1fr; - gap: 40px; - } + .hero__inner, + .about__inner, + .register__inner { + grid-template-columns: 1fr; + gap: 40px; + } - .globe-card { - min-height: 360px; - } - - .footer__inner { - grid-template-columns: 1fr; - } - .footer__center, - .footer__right { - justify-self: start; - text-align: left; - } - .socials { - justify-content: flex-start; - } - .footer__right { - justify-content: flex-start; - } - .mini-globe-wrap { - max-width: 260px; - } + .globe-card { min-height: 360px; } + .mini-globe-wrap { max-width: 260px; } } @media (max-width: 680px) { - .container { - padding: 0 18px; - } - .hero { - padding: 50px 0 40px; - } - .about, - .register { - padding: 70px 0; - } - .nav__link { - display: none; - } - .register__card { - padding: 28px 22px; - } - .globe-card { - padding: 24px; - min-height: 280px; - } - - .header { - height: auto; - padding: 14px 0; - } - :root { - --hh: 64px; - } + .hero { padding: 50px 0 40px; } + .about, + .register { padding: 70px 0; } + .register__card { padding: 28px 22px; } + .globe-card { padding: 24px; min-height: 280px; } } @media (max-width: 420px) { - .hero__title { - font-size: 3rem; - } - .section-title { - font-size: 2.2rem; - } - .btn { - padding: 14px 22px; - font-size: 0.95rem; - } + .hero__title { font-size: 3rem; } } @media (max-width: 360px) { - :root { - --r-xl: 22px; - --r-lg: 16px; - } - - .container { - padding: 0 14px; - } - - .logo__mark { - width: 36px; - height: 36px; - } - .logo__text { - font-size: 1.05rem; - } - .nav__cta { - padding: 8px 14px; - font-size: 0.84rem; - margin-left: 0; - } - - .hero { - padding: 36px 0 32px; - min-height: auto; - } - .hero__title { - font-size: 2.6rem; - letter-spacing: -0.03em; - } - .hero__desc { - font-size: 0.95rem; - margin-bottom: 28px; - } - - .globe-card { - padding: 16px; - min-height: 240px; - } - .globe-badge { - padding: 7px 10px; - font-size: 0.72rem; - } - - .about, - .register { - padding: 52px 0; - } - .section-title { - font-size: 2rem; - } - .about__desc { - font-size: 0.95rem; - } - - .step { - padding: 16px 18px; - gap: 14px; - } - .step__num { - width: 46px; - height: 46px; - font-size: 0.88rem; - border-radius: 12px; - } - - .register__card { - padding: 22px 16px; - border-radius: var(--r-lg); - } - .card-title { - font-size: 1.25rem; - margin-bottom: 22px; - } - .field input { - height: 48px; - font-size: 0.92rem; - } - .btn--full { - padding: 14px 18px; - font-size: 0.95rem; - } - - .footer { - padding: 30px 0; - } - .footer__brand { - font-size: 1.25rem; - } - .social { - width: 36px; - height: 36px; - font-size: 0.8rem; - } - .socials { - gap: 8px; - } - .footer__right { - gap: 14px; - } -} - -/* ─── SCROLL REVEAL ─── */ -.reveal { - opacity: 0; - transform: translateY(28px); - transition: - opacity 0.6s ease, - transform 0.6s ease; -} - -.reveal.visible { - opacity: 1; - transform: translateY(0); -} - -.reveal-delay-1 { - transition-delay: 0.1s; -} -.reveal-delay-2 { - transition-delay: 0.2s; -} -.reveal-delay-3 { - transition-delay: 0.32s; + .hero { padding: 36px 0 32px; min-height: auto; } + .hero__title { font-size: 2.6rem; letter-spacing: -0.03em; } + .hero__desc { font-size: 0.95rem; margin-bottom: 28px; } + .globe-card { padding: 16px; min-height: 240px; } + .globe-badge { padding: 7px 10px; font-size: 0.72rem; } + .about, + .register { padding: 52px 0; } + .about__desc { font-size: 0.95rem; } + .step { padding: 16px 18px; gap: 14px; } + .step__num { width: 46px; height: 46px; font-size: 0.88rem; border-radius: 12px; } + .register__card { padding: 22px 16px; border-radius: var(--r-lg); } + .card-title { font-size: 1.25rem; margin-bottom: 22px; } + .btn--full { padding: 14px 18px; font-size: 0.95rem; } } diff --git a/frontend/styles/leader.css b/frontend/styles/leader.css index bb04279..76b62f8 100644 --- a/frontend/styles/leader.css +++ b/frontend/styles/leader.css @@ -1,196 +1,193 @@ +/* leader.css — leaderboard page styles */ + .lb-page { - min-height: calc(100vh - var(--hh)); - padding: 60px 0; - } + min-height: calc(100vh - var(--hh)); + padding: 60px 0; +} - .lb-inner { - max-width: 760px; - margin: 0 auto; - } +.lb-inner { + max-width: 760px; + margin: 0 auto; +} - .lb-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 20px; - margin-bottom: 32px; - flex-wrap: wrap; - } +.lb-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 20px; + margin-bottom: 32px; + flex-wrap: wrap; +} - .lb-title { - font-family: 'Syne', sans-serif; - font-weight: 800; - font-size: clamp(2rem, 5vw, 3.2rem); - letter-spacing: -.03em; - line-height: 1; - } +.lb-title { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: clamp(2rem, 5vw, 3.2rem); + letter-spacing: -0.03em; + line-height: 1; +} - .lb-title em { - font-style: normal; - background: linear-gradient(135deg, var(--sea), var(--leaf)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } +.lb-title em { + font-style: normal; + background: linear-gradient(135deg, var(--sea), var(--leaf)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} - .lb-actions { display: flex; gap: 10px; flex-wrap: wrap; } +.lb-actions { display: flex; gap: 10px; flex-wrap: wrap; } - /* Table */ - .lb-table { - background: var(--white); - border: 1px solid var(--line); - border-radius: var(--r-xl); - overflow: hidden; - box-shadow: var(--shadow); - } +/* ─── TABLE ─── */ +.lb-table { + background: var(--white); + border: 1px solid var(--line); + border-radius: var(--r-xl); + overflow: hidden; + box-shadow: var(--shadow); +} - .lb-table-head { - display: grid; - grid-template-columns: 56px 1fr 90px 90px 90px; - gap: 0; - padding: 14px 24px; - background: var(--cream); - border-bottom: 1px solid var(--line); - } +.lb-table-head { + display: grid; + grid-template-columns: 56px 1fr 90px 90px 90px; + padding: 14px 24px; + background: var(--cream); + border-bottom: 1px solid var(--line); +} - .lb-th { - font-size: .72rem; - font-weight: 700; - letter-spacing: .08em; - text-transform: uppercase; - color: var(--ink-muted); - } +.lb-th { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted); +} - .lb-th.right { text-align: right; } +.lb-th.right { text-align: right; } - .lb-body { display: grid; } +.lb-body { display: grid; } - .lb-row { - display: grid; - grid-template-columns: 56px 1fr 90px 90px 90px; - align-items: center; - padding: 16px 24px; - border-bottom: 1px solid var(--line); - transition: background .15s; - animation: fadeRow .4s ease both; - } +.lb-row { + display: grid; + grid-template-columns: 56px 1fr 90px 90px 90px; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid var(--line); + transition: background 0.15s; + animation: fadeRow 0.4s ease both; +} - .lb-row:last-child { border-bottom: none; } +.lb-row:last-child { border-bottom: none; } +.lb-row:hover { background: var(--cream); } - .lb-row:hover { background: var(--cream); } +@keyframes fadeRow { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} - @keyframes fadeRow { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } - } +/* ─── TOP 3 HIGHLIGHT ─── */ +.lb-row.rank-1 { background: rgba(240, 180, 40, 0.07); } +.lb-row.rank-2 { background: rgba(180, 180, 195, 0.06); } +.lb-row.rank-3 { background: rgba(205, 130, 70, 0.05); } - /* Top 3 highlight */ - .lb-row.rank-1 { background: rgba(240,180,40,.07); } - .lb-row.rank-2 { background: rgba(180,180,195,.06); } - .lb-row.rank-3 { background: rgba(205,130,70,.05); } +.lb-rank { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 1rem; + color: var(--ink-muted); +} - .lb-rank { - font-family: 'Syne', sans-serif; - font-weight: 800; - font-size: 1rem; - color: var(--ink-muted); - } +.lb-row.rank-1 .lb-rank { color: var(--gold); } +.lb-row.rank-2 .lb-rank { color: #a0a0b0; } +.lb-row.rank-3 .lb-rank { color: #c07840; } - .lb-row.rank-1 .lb-rank { color: var(--gold); } - .lb-row.rank-2 .lb-rank { color: #a0a0b0; } - .lb-row.rank-3 .lb-rank { color: #c07840; } +.lb-medal { font-size: 1.1rem; } - .lb-medal { font-size: 1.1rem; } +.lb-name { + font-family: "Syne", sans-serif; + font-weight: 700; + font-size: 0.96rem; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} - .lb-name { - font-family: 'Syne', sans-serif; - font-weight: 700; - font-size: .96rem; - color: var(--ink); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } +.lb-name.is-you::after { + content: " (you)"; + font-weight: 400; + font-family: "DM Sans", sans-serif; + color: var(--ink-muted); + font-size: 0.8rem; +} - .lb-name.is-you::after { - content: ' (you)'; - font-weight: 400; - font-family: 'DM Sans', sans-serif; - color: var(--ink-muted); - font-size: .8rem; - } +.lb-rounds { + font-size: 0.82rem; + color: var(--ink-muted); + text-align: right; +} - .lb-rounds { - font-size: .82rem; - color: var(--ink-muted); - text-align: right; - } +.lb-date { + font-size: 0.78rem; + color: var(--ink-muted); + text-align: right; +} - .lb-date { - font-size: .78rem; - color: var(--ink-muted); - text-align: right; - } +.lb-score { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 1.05rem; + color: var(--sea); + text-align: right; +} - .lb-score { - font-family: 'Syne', sans-serif; - font-weight: 800; - font-size: 1.05rem; - color: var(--sea); - text-align: right; - } +.lb-score.gold { color: var(--gold); } +.lb-score.silver { color: #888898; } +.lb-score.bronze { color: #c07840; } - .lb-score.gold { color: var(--gold); } - .lb-score.silver { color: #888898; } - .lb-score.bronze { color: #c07840; } +/* ─── EMPTY STATE ─── */ +.lb-empty { + padding: 64px 24px; + text-align: center; +} - /* Empty state */ - .lb-empty { - padding: 64px 24px; - text-align: center; - } +.lb-empty__icon { font-size: 3rem; margin-bottom: 12px; } - .lb-empty__icon { font-size: 3rem; margin-bottom: 12px; } +.lb-empty__text { + font-size: 1rem; + color: var(--ink-soft); + margin-bottom: 20px; +} - .lb-empty__text { - font-size: 1rem; - color: var(--ink-soft); - margin-bottom: 20px; - } +/* ─── CURRENT PLAYER BAR ─── */ +.your-score-bar { + margin-top: 20px; + padding: 16px 24px; + background: linear-gradient(135deg, var(--sea-dim), var(--leaf-dim)); + border: 1px solid rgba(26, 127, 196, 0.18); + border-radius: var(--r-lg); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} - /* Current player highlight bar */ - .your-score-bar { - margin-top: 20px; - padding: 16px 24px; - background: linear-gradient(135deg, var(--sea-dim), var(--leaf-dim)); - border: 1px solid rgba(26,127,196,.18); - border-radius: var(--r-lg); - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - flex-wrap: wrap; - } +.your-score-bar__text { font-size: 0.9rem; color: var(--ink-soft); } +.your-score-bar__text strong { color: var(--ink); } - .your-score-bar__text { - font-size: .9rem; - color: var(--ink-soft); - } +/* ─── RESPONSIVE ─── */ +@media (max-width: 640px) { + .lb-page { padding: 40px 0; } - .your-score-bar__text strong { color: var(--ink); } + .lb-table-head, + .lb-row { grid-template-columns: 44px 1fr 72px; } - @media (max-width: 640px) { - .lb-page { padding: 40px 0; } - .lb-table-head, - .lb-row { - grid-template-columns: 44px 1fr 72px; - } - .lb-th.hide-sm, - .lb-rounds, - .lb-date { display: none; } - } + .lb-th.hide-sm, + .lb-rounds, + .lb-date { display: none; } +} - @media (max-width: 360px) { - .lb-table-head, - .lb-row { padding: 12px 16px; } - } \ No newline at end of file +@media (max-width: 360px) { + .lb-table-head, + .lb-row { padding: 12px 16px; } +} diff --git a/frontend/styles/lobby.css b/frontend/styles/lobby.css index 46a73c4..2714da8 100644 --- a/frontend/styles/lobby.css +++ b/frontend/styles/lobby.css @@ -142,49 +142,7 @@ margin-bottom: 30px; } -.form { display: grid; gap: 20px; } - -.field { display: grid; gap: 8px; } - -.field label { - font-size: .84rem; - font-weight: 600; - color: var(--ink-soft); - letter-spacing: .02em; -} - -.field input { - width: 100%; - height: 54px; - padding: 0 18px; - border: 1.5px solid var(--line); - border-radius: var(--r-md); - background: var(--cream); - color: var(--ink); - font-size: .98rem; - transition: border-color .18s, box-shadow .18s; -} - -.field input::placeholder { color: var(--ink-muted); } - -.field input:focus { - outline: none; - border-color: var(--sea-light); - box-shadow: 0 0 0 3px rgba(26,127,196,.12); - background: var(--white); -} - -.field input.input--error { - border-color: var(--danger); - box-shadow: 0 0 0 3px rgba(224,92,92,.12); -} - -.field-error { - font-size: .82rem; - color: var(--danger); - min-height: 1.2em; - transition: opacity .18s; -} +/* Form and field styles are defined in main.css */ /* ─── Responsive ─── */ @media (max-width: 900px) { diff --git a/frontend/styles/main.css b/frontend/styles/main.css index 2058270..87f216d 100644 --- a/frontend/styles/main.css +++ b/frontend/styles/main.css @@ -253,6 +253,50 @@ button, input { font: inherit; } margin-bottom: 16px; } +/* ─── FORM ─── */ +.form { display: grid; gap: 20px; } + +.field { display: grid; gap: 8px; } + +.field label { + font-size: 0.84rem; + font-weight: 600; + color: var(--ink-soft); + letter-spacing: 0.02em; +} + +.field input { + width: 100%; + height: 54px; + padding: 0 18px; + border: 1.5px solid var(--line); + border-radius: var(--r-md); + background: var(--cream); + color: var(--ink); + font-size: 0.98rem; + transition: border-color 0.18s, box-shadow 0.18s; +} + +.field input::placeholder { color: var(--ink-muted); } + +.field input:focus { + outline: none; + border-color: var(--sea-light); + box-shadow: 0 0 0 3px rgba(26, 127, 196, 0.12); + background: var(--white); +} + +.field input.input--error { + border-color: var(--danger); + box-shadow: 0 0 0 3px rgba(224, 92, 92, 0.12); +} + +.field-error { + font-size: 0.82rem; + color: var(--danger); + min-height: 1.2em; +} + /* ─── FOOTER ─── */ .footer { border-top: 1px solid var(--line); diff --git a/frontend/styles/results.css b/frontend/styles/results.css new file mode 100644 index 0000000..297333e --- /dev/null +++ b/frontend/styles/results.css @@ -0,0 +1,196 @@ +/* results.css — results page styles */ + +.results-page { + min-height: calc(100vh - var(--hh)); + display: grid; + place-items: center; + padding: 60px 0; +} + +.results-inner { + width: 100%; + max-width: 680px; + display: grid; + gap: 24px; +} + +/* ─── HEADER ─── */ +.results-header { + text-align: center; +} + +.results-emoji { + font-size: 3.5rem; + margin-bottom: 16px; + display: block; + animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +@keyframes popIn { + from { transform: scale(0); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.results-title { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: clamp(2rem, 5vw, 3.4rem); + letter-spacing: -0.03em; + line-height: 1.05; + margin-bottom: 8px; +} + +.results-player { + font-size: 1rem; + color: var(--ink-soft); +} + +.results-player strong { color: var(--ink); } + +/* ─── TOTAL SCORE ─── */ +.score-total-card { + background: linear-gradient(135deg, var(--sea) 0%, #159fd4 50%, var(--leaf) 100%); + border-radius: var(--r-xl); + padding: 36px 40px; + text-align: center; + color: var(--white); + box-shadow: 0 16px 40px rgba(26, 127, 196, 0.3); + animation: slideUp 0.5s 0.1s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +@keyframes slideUp { + from { transform: translateY(30px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.score-total-label { + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + opacity: 0.78; + margin-bottom: 8px; +} + +.score-total-num { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: clamp(4rem, 10vw, 6rem); + line-height: 1; + letter-spacing: -0.04em; +} + +.score-total-max { + font-size: 1.4rem; + opacity: 0.6; + font-weight: 400; +} + +.score-grade { + display: inline-block; + margin-top: 12px; + padding: 4px 18px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.22); + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 1.1rem; + letter-spacing: 0.04em; +} + +/* ─── ROUND BREAKDOWN ─── */ +.rounds-card { + background: var(--white); + border: 1px solid var(--line); + border-radius: var(--r-xl); + padding: 28px 32px; + box-shadow: var(--shadow-sm); + animation: slideUp 0.5s 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +.rounds-card-title { + font-family: "Syne", sans-serif; + font-weight: 700; + font-size: 1rem; + letter-spacing: -0.01em; + margin-bottom: 20px; + color: var(--ink-soft); +} + +.round-rows { display: grid; gap: 14px; } + +.round-row { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 16px; +} + +.round-row__label { + font-size: 0.82rem; + font-weight: 600; + color: var(--ink-soft); + white-space: nowrap; +} + +.round-row__bar-wrap { + height: 8px; + background: var(--cream); + border-radius: 999px; + overflow: hidden; +} + +.round-row__bar { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--sea), var(--leaf)); + transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.round-row__score { + font-family: "Syne", sans-serif; + font-weight: 800; + font-size: 0.9rem; + color: var(--sea); + min-width: 36px; + text-align: right; +} + +/* ─── COUNTRY TAGS ─── */ +.countries-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.country-tag { + padding: 5px 12px; + border-radius: 999px; + background: var(--sea-dim); + color: var(--sea); + font-size: 0.8rem; + font-weight: 600; +} + +/* ─── ACTIONS ─── */ +.results-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + animation: slideUp 0.5s 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +.results-actions .btn { flex: 1 1 180px; } + +/* ─── RESPONSIVE ─── */ +@media (max-width: 600px) { + .results-page { padding: 40px 0; } + .score-total-card { padding: 28px 24px; } + .rounds-card { padding: 22px 20px; } + .results-actions .btn { flex-basis: 100%; } +} + +@media (max-width: 360px) { + .score-total-num { font-size: 3.4rem; } +} From a1be788d36626da3b279f030769071a5f3cab153 Mon Sep 17 00:00:00 2001 From: pengniklas Date: Fri, 22 May 2026 16:42:25 +0200 Subject: [PATCH 3/4] add JSDoc types, fix timer CSS classes, safe storage writes --- frontend/scripts/countries.js | 180 +++++++++++++++++++--------------- frontend/scripts/drawing.js | 92 ++++++++++------- frontend/scripts/game.js | 77 +++++++-------- frontend/scripts/index.js | 48 +++++---- frontend/scripts/lobby.js | 14 ++- frontend/scripts/scoring.js | 36 ++++--- frontend/scripts/storage.js | 82 +++++++++------- frontend/styles/game.css | 9 ++ 8 files changed, 307 insertions(+), 231 deletions(-) diff --git a/frontend/scripts/countries.js b/frontend/scripts/countries.js index 1b77e2f..6b6bb8e 100644 --- a/frontend/scripts/countries.js +++ b/frontend/scripts/countries.js @@ -1,115 +1,135 @@ -// countries.js — country data helpers (inline, no fetch needed) +// countries.js — country data and helpers + +/** + * @typedef {{ name: string, x: number, y: number }} City + * @typedef {{ name: string, hint: string, cities: City[] }} Country + */ const Countries = (() => { + /** @type {Country[]} */ const COUNTRIES_DATA = [ { - "name": "Switzerland", - "hint": "Alpine country in Central Europe", - "cities": [ - { "name": "Bern", "x": 48, "y": 58 }, - { "name": "Zürich", "x": 58, "y": 38 }, - { "name": "Geneva", "x": 22, "y": 72 } - ] + name: "Switzerland", + hint: "Alpine country in Central Europe", + cities: [ + { name: "Bern", x: 48, y: 58 }, + { name: "Zürich", x: 58, y: 38 }, + { name: "Geneva", x: 22, y: 72 }, + ], }, { - "name": "Norway", - "hint": "Scandinavian country with long coastline", - "cities": [ - { "name": "Oslo", "x": 55, "y": 72 }, - { "name": "Bergen", "x": 32, "y": 60 }, - { "name": "Tromsø", "x": 62, "y": 18 } - ] + name: "Norway", + hint: "Scandinavian country with long coastline", + cities: [ + { name: "Oslo", x: 55, y: 72 }, + { name: "Bergen", x: 32, y: 60 }, + { name: "Tromsø", x: 62, y: 18 }, + ], }, { - "name": "Italy", - "hint": "Boot-shaped peninsula in Southern Europe", - "cities": [ - { "name": "Rome", "x": 52, "y": 58 }, - { "name": "Milan", "x": 42, "y": 22 }, - { "name": "Naples", "x": 58, "y": 72 } - ] + name: "Italy", + hint: "Boot-shaped peninsula in Southern Europe", + cities: [ + { name: "Rome", x: 52, y: 58 }, + { name: "Milan", x: 42, y: 22 }, + { name: "Naples", x: 58, y: 72 }, + ], }, { - "name": "Japan", - "hint": "Island nation in East Asia", - "cities": [ - { "name": "Tokyo", "x": 72, "y": 48 }, - { "name": "Osaka", "x": 58, "y": 58 }, - { "name": "Sapporo", "x": 70, "y": 22 } - ] + name: "Japan", + hint: "Island nation in East Asia", + cities: [ + { name: "Tokyo", x: 72, y: 48 }, + { name: "Osaka", x: 58, y: 58 }, + { name: "Sapporo", x: 70, y: 22 }, + ], }, { - "name": "Brazil", - "hint": "Largest country in South America", - "cities": [ - { "name": "Brasília", "x": 58, "y": 52 }, - { "name": "São Paulo", "x": 60, "y": 68 }, - { "name": "Manaus", "x": 38, "y": 38 } - ] + name: "Brazil", + hint: "Largest country in South America", + cities: [ + { name: "Brasília", x: 58, y: 52 }, + { name: "São Paulo", x: 60, y: 68 }, + { name: "Manaus", x: 38, y: 38 }, + ], }, { - "name": "Australia", - "hint": "Continent and country in the Southern Hemisphere", - "cities": [ - { "name": "Canberra", "x": 72, "y": 72 }, - { "name": "Sydney", "x": 78, "y": 68 }, - { "name": "Perth", "x": 22, "y": 65 } - ] + name: "Australia", + hint: "Continent and country in the Southern Hemisphere", + cities: [ + { name: "Canberra", x: 72, y: 72 }, + { name: "Sydney", x: 78, y: 68 }, + { name: "Perth", x: 22, y: 65 }, + ], }, { - "name": "France", - "hint": "Western Europe, roughly hexagonal shape", - "cities": [ - { "name": "Paris", "x": 50, "y": 32 }, - { "name": "Lyon", "x": 58, "y": 55 }, - { "name": "Marseille", "x": 58, "y": 72 } - ] + name: "France", + hint: "Western Europe, roughly hexagonal shape", + cities: [ + { name: "Paris", x: 50, y: 32 }, + { name: "Lyon", x: 58, y: 55 }, + { name: "Marseille", x: 58, y: 72 }, + ], }, { - "name": "India", - "hint": "Large peninsula in South Asia", - "cities": [ - { "name": "New Delhi", "x": 46, "y": 28 }, - { "name": "Mumbai", "x": 32, "y": 55 }, - { "name": "Chennai", "x": 52, "y": 72 } - ] + name: "India", + hint: "Large peninsula in South Asia", + cities: [ + { name: "New Delhi", x: 46, y: 28 }, + { name: "Mumbai", x: 32, y: 55 }, + { name: "Chennai", x: 52, y: 72 }, + ], }, { - "name": "Canada", - "hint": "Second largest country in the world", - "cities": [ - { "name": "Ottawa", "x": 62, "y": 52 }, - { "name": "Vancouver", "x": 22, "y": 55 }, - { "name": "Toronto", "x": 60, "y": 58 } - ] + name: "Canada", + hint: "Second largest country in the world", + cities: [ + { name: "Ottawa", x: 62, y: 52 }, + { name: "Vancouver", x: 22, y: 55 }, + { name: "Toronto", x: 60, y: 58 }, + ], }, { - "name": "Germany", - "hint": "Central European country", - "cities": [ - { "name": "Berlin", "x": 58, "y": 28 }, - { "name": "Munich", "x": 48, "y": 68 }, - { "name": "Hamburg", "x": 42, "y": 18 } - ] - } + name: "Germany", + hint: "Central European country", + cities: [ + { name: "Berlin", x: 58, y: 28 }, + { name: "Munich", x: 48, y: 68 }, + { name: "Hamburg", x: 42, y: 18 }, + ], + }, ]; + /** @type {Country[]} */ let _data = []; - async function loadCountries() { - if (_data.length) return _data; - _data = COUNTRIES_DATA; - return _data; + /** + * Load country data into memory. Safe to call multiple times. + * Returns a Promise for future compatibility with a real API fetch. + * @returns {Promise} + */ + function loadCountries() { + if (!_data.length) _data = COUNTRIES_DATA; + return Promise.resolve(_data); } + /** + * Return a random subset of countries. + * @param {number} count + * @returns {Country[]} + */ function getRandomCountries(count = 3) { - const shuffled = [..._data].sort(() => Math.random() - 0.5); - return shuffled.slice(0, count); + return [..._data].sort(() => Math.random() - 0.5).slice(0, count); } + /** + * Get city list for a specific country by name. + * @param {string} countryName + * @returns {City[]} + */ function getCities(countryName) { - const c = _data.find(c => c.name === countryName); - return c ? c.cities : []; + const country = _data.find((c) => c.name === countryName); + return country ? country.cities : []; } return { loadCountries, getRandomCountries, getCities }; diff --git a/frontend/scripts/drawing.js b/frontend/scripts/drawing.js index a1ac3a2..ec173ac 100644 --- a/frontend/scripts/drawing.js +++ b/frontend/scripts/drawing.js @@ -1,22 +1,31 @@ // drawing.js — canvas drawing module const Drawing = (() => { - let canvas, ctx; - let isDrawing = false; - let points = []; // [{x, y}, ...] - let cities = []; // [{name, x, y}, ...] (% coords) + /** @type {HTMLCanvasElement} */ + let canvas; + /** @type {CanvasRenderingContext2D} */ + let ctx; + + let isDrawing = false; + /** @type {{ x: number, y: number }[]} */ + let points = []; + /** @type {{ name: string, x: number, y: number }[]} */ + let cities = []; const STROKE_COLOR = "#1a7fc4"; const STROKE_WIDTH = 2.5; + /** + * Initialise the drawing module on a canvas element. + * @param {HTMLCanvasElement} canvasEl + */ function init(canvasEl) { canvas = canvasEl; ctx = canvas.getContext("2d"); - // Pointer events (mouse + touch) - canvas.addEventListener("pointerdown", onDown); - canvas.addEventListener("pointermove", onMove); - canvas.addEventListener("pointerup", onUp); + canvas.addEventListener("pointerdown", onDown); + canvas.addEventListener("pointermove", onMove); + canvas.addEventListener("pointerup", onUp); canvas.addEventListener("pointerleave", onUp); canvas.style.touchAction = "none"; @@ -24,6 +33,7 @@ const Drawing = (() => { window.addEventListener("resize", _resize); } + /** Resize canvas to match its CSS size, accounting for device pixel ratio. */ function _resize() { if (!canvas) return; const { width, height } = canvas.getBoundingClientRect(); @@ -33,14 +43,17 @@ const Drawing = (() => { _redraw(); } + /** + * Convert a pointer event to canvas-local coordinates. + * @param {PointerEvent} e + * @returns {{ x: number, y: number }} + */ function _pos(e) { - const r = canvas.getBoundingClientRect(); - return { - x: (e.clientX - r.left), - y: (e.clientY - r.top), - }; + const rect = canvas.getBoundingClientRect(); + return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } + /** @param {PointerEvent} e */ function onDown(e) { e.preventDefault(); isDrawing = true; @@ -50,6 +63,7 @@ const Drawing = (() => { ctx.moveTo(p.x, p.y); } + /** @param {PointerEvent} e */ function onMove(e) { if (!isDrawing) return; e.preventDefault(); @@ -63,54 +77,55 @@ const Drawing = (() => { ctx.stroke(); } + /** @param {PointerEvent} e */ function onUp(e) { if (!isDrawing) return; isDrawing = false; e.preventDefault(); } + /** Clear the canvas and redraw city markers. */ function clear() { points = []; if (!ctx) return; - const w = canvas.getBoundingClientRect().width; - const h = canvas.getBoundingClientRect().height; - ctx.clearRect(0, 0, w, h); + const { width, height } = canvas.getBoundingClientRect(); + ctx.clearRect(0, 0, width, height); _drawCities(); } + /** + * Set city markers to display on the canvas. + * @param {{ name: string, x: number, y: number }[]} cityList - Coords in percent (0–100). + */ function setCities(cityList) { cities = cityList || []; _drawCities(); } + /** Render all city markers with labels. */ function _drawCities() { if (!ctx || !cities.length) return; - const w = canvas.getBoundingClientRect().width; - const h = canvas.getBoundingClientRect().height; + const { width, height } = canvas.getBoundingClientRect(); - cities.forEach(city => { - const cx = (city.x / 100) * w; - const cy = (city.y / 100) * h; + cities.forEach((city) => { + const cx = (city.x / 100) * width; + const cy = (city.y / 100) * height; // Dot ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); - ctx.fillStyle = "rgba(240,180,40,0.9)"; + ctx.fillStyle = "rgba(240,180,40,0.9)"; ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.9)"; - ctx.lineWidth = 1.5; + ctx.lineWidth = 1.5; ctx.stroke(); - // Label + // White pill label background ctx.font = "bold 11px 'DM Sans', sans-serif"; - ctx.fillStyle = "#0b1f2a"; - ctx.textAlign = "center"; - - // White pill background const textW = ctx.measureText(city.name).width + 10; const textH = 16; - const tx = cx; - const ty = cy - 14; + const tx = cx; + const ty = cy - 14; ctx.save(); ctx.fillStyle = "rgba(255,255,255,0.88)"; @@ -120,10 +135,12 @@ const Drawing = (() => { ctx.restore(); ctx.fillStyle = "#0b1f2a"; + ctx.textAlign = "center"; ctx.fillText(city.name, tx, ty + 4); }); } + /** Redraw the full stroke from stored points. */ function _redraw() { _drawCities(); if (!points.length) return; @@ -132,17 +149,18 @@ const Drawing = (() => { ctx.lineWidth = STROKE_WIDTH; ctx.lineJoin = "round"; ctx.lineCap = "round"; - points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)); + points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y))); ctx.stroke(); } - function getPoints() { - return [...points]; - } + /** + * Return a copy of the current drawn points. + * @returns {{ x: number, y: number }[]} + */ + function getPoints() { return [...points]; } - function destroy() { - window.removeEventListener("resize", _resize); - } + /** Remove event listeners and clean up. */ + function destroy() { window.removeEventListener("resize", _resize); } return { init, clear, setCities, getPoints, destroy }; })(); diff --git a/frontend/scripts/game.js b/frontend/scripts/game.js index 5438b48..380394c 100644 --- a/frontend/scripts/game.js +++ b/frontend/scripts/game.js @@ -3,8 +3,10 @@ const TOTAL_ROUNDS = 3; const ROUND_DURATION = 60; // seconds +/** @type {import('./countries.js').Country[]} */ let roundCountries = []; let currentRound = 0; +/** @type {number[]} */ let scores = []; let timerInterval = null; let timeLeft = ROUND_DURATION; @@ -15,10 +17,13 @@ const elCountryHint = document.getElementById("country-hint"); const elRoundNum = document.getElementById("round-num"); const elTimerNum = document.getElementById("timer-num"); const elTimerBar = document.getElementById("timer-bar"); +const elTimerWrap = document.querySelector(".game-timer"); const elBtnClear = document.getElementById("btn-clear"); const elBtnSubmit = document.getElementById("btn-submit"); // ── Init + +/** Load countries and start the first round. */ async function initGame() { await Countries.loadCountries(); roundCountries = Countries.getRandomCountries(TOTAL_ROUNDS); @@ -28,66 +33,61 @@ async function initGame() { } // ── Round + +/** Set up UI and timer for the current round. */ function startRound() { const country = roundCountries[currentRound]; - // Update UI elRoundNum.textContent = currentRound + 1; elCountryName.textContent = country.name; elCountryHint.textContent = country.hint || ""; - // Round pips if (typeof window.updateRoundPips === "function") { window.updateRoundPips(currentRound + 1); } - // Reset canvas placeholder document.getElementById("canvas-wrap")?.classList.remove("has-drawing"); - // Cities on canvas Drawing.clear(); Drawing.setCities(country.cities || []); - // Timer timeLeft = ROUND_DURATION; updateTimerUI(); clearInterval(timerInterval); timerInterval = setInterval(tickTimer, 1000); - // Button state - elBtnSubmit.disabled = false; + elBtnSubmit.disabled = false; elBtnSubmit.textContent = currentRound < TOTAL_ROUNDS - 1 ? "Submit & Next Round →" : "Submit & See Results →"; } +/** Decrement timer by one second and auto-submit when time runs out. */ function tickTimer() { timeLeft--; updateTimerUI(); if (timeLeft <= 0) { clearInterval(timerInterval); - submitRound(true); // auto-submit + submitRound(true); } } +/** + * Sync timer bar width and apply urgency CSS classes. + * Uses `.timer--warning` and `.timer--danger` instead of inline styles. + */ function updateTimerUI() { - elTimerNum.textContent = timeLeft; - const pct = (timeLeft / ROUND_DURATION) * 100; - elTimerBar.style.width = pct + "%"; + elTimerNum.textContent = timeLeft; + elTimerBar.style.width = `${(timeLeft / ROUND_DURATION) * 100}%`; - // Colour shift - if (timeLeft <= 10) { - elTimerBar.style.background = "#e05c5c"; - elTimerNum.style.color = "#e05c5c"; - } else if (timeLeft <= 20) { - elTimerBar.style.background = "#f0b429"; - elTimerNum.style.color = "#f0b429"; - } else { - elTimerBar.style.background = ""; - elTimerNum.style.color = ""; - } + elTimerWrap.classList.toggle("timer--danger", timeLeft <= 10); + elTimerWrap.classList.toggle("timer--warning", timeLeft > 10 && timeLeft <= 20); } +/** + * Submit the current round, record the score, and advance or finish. + * @param {boolean} [auto=false] - True when triggered by timer expiry. + */ function submitRound(auto = false) { clearInterval(timerInterval); elBtnSubmit.disabled = true; @@ -96,15 +96,12 @@ function submitRound(auto = false) { const score = Scoring.calculateScore(points); scores.push(score); - // Update sidebar score row if (typeof window.updateScoreDisplay === "function") { window.updateScoreDisplay(currentRound, score); } - // Flash score feedback showScoreFeedback(score); - const delay = auto ? 400 : 1200; setTimeout(() => { if (currentRound < TOTAL_ROUNDS - 1) { currentRound++; @@ -112,45 +109,47 @@ function submitRound(auto = false) { } else { finishGame(); } - }, delay); + }, auto ? 400 : 1200); } +/** + * Briefly display the score grade overlay on the canvas. + * @param {number} score + */ function showScoreFeedback(score) { - const grade = Scoring.getGrade(score); - const el = document.getElementById("score-feedback"); - el.textContent = `${score}% ${grade.label}`; - el.style.color = grade.color; - el.style.opacity = "1"; - el.style.transform = "translateY(0)"; + const grade = Scoring.getGrade(score); + const el = document.getElementById("score-feedback"); + el.textContent = `${score}% ${grade.label}`; + el.style.color = grade.color; + el.style.opacity = "1"; + el.style.transform = "translateY(0)"; setTimeout(() => { - el.style.opacity = "0"; + el.style.opacity = "0"; el.style.transform = "translateY(-10px)"; }, 900); } +/** Persist game state, update leaderboard, and navigate to results. */ function finishGame() { - const totalScore = scores.reduce((a, b) => a + b, 0); + const totalScore = scores.reduce((sum, s) => sum + s, 0); const state = { currentRound: TOTAL_ROUNDS, scores, totalScore, - countries: roundCountries.map(c => c.name), + countries: roundCountries.map((c) => c.name), }; Storage.saveGameState(state); - - // Save to leaderboard Storage.saveLeaderboard({ name: Storage.getPlayerName(), totalScore, scores, date: new Date().toISOString(), }); - location.href = "results.html"; } // ── Events -elBtnClear.addEventListener("click", () => Drawing.clear()); +elBtnClear.addEventListener("click", () => Drawing.clear()); elBtnSubmit.addEventListener("click", () => submitRound(false)); // ── Boot diff --git a/frontend/scripts/index.js b/frontend/scripts/index.js index 0a34835..902e5b4 100644 --- a/frontend/scripts/index.js +++ b/frontend/scripts/index.js @@ -1,27 +1,33 @@ +// index.js — landing page logic + +/** + * Handle the "Create game" button click. + * Validates the lobby name, persists it, and navigates to the lobby page. + */ document.getElementById("reg-btn")?.addEventListener("click", () => { - const lobbyInput = document.getElementById("username"); - const lobbyName = lobbyInput ? lobbyInput.value.trim() : ""; - if (lobbyName) { - Storage.saveLobbyName(lobbyName); - window.location.href = "lobby.html"; - } else { - lobbyInput?.focus(); - } + const lobbyInput = /** @type {HTMLInputElement|null} */ (document.getElementById("username")); + const lobbyName = lobbyInput ? lobbyInput.value.trim() : ""; + + if (lobbyName) { + Storage.saveLobbyName(lobbyName); + window.location.href = "lobby.html"; + } else { + lobbyInput?.focus(); + } }); +// Scroll reveal — animate elements into view as they enter the viewport const reveals = document.querySelectorAll(".reveal"); -const io = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add("visible"); - io.unobserve(entry.target); - } - }); - }, - { threshold: 0.12 }, +const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible"); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.12 }, ); -reveals.forEach((el) => { - io.observe(el); -}); +reveals.forEach((el) => observer.observe(el)); diff --git a/frontend/scripts/lobby.js b/frontend/scripts/lobby.js index 06289a4..75ca033 100644 --- a/frontend/scripts/lobby.js +++ b/frontend/scripts/lobby.js @@ -1,18 +1,23 @@ -// lobby.js +// lobby.js — lobby page logic document.addEventListener("DOMContentLoaded", () => { const input = document.getElementById("username"); const btn = document.getElementById("btn-start"); const errMsg = document.getElementById("name-error"); - // Show lobby name + // Display the current lobby name const lobbyNameEl = document.getElementById("lobby-name-display"); if (lobbyNameEl) { lobbyNameEl.textContent = Storage.getLobbyName(); } + /** + * Validate the username input and navigate to the game. + * Shows an inline error message on invalid input. + */ btn.addEventListener("click", () => { const name = input.value.trim(); + if (!name) { errMsg.textContent = "Please enter a username to continue."; input.classList.add("input--error"); @@ -25,21 +30,24 @@ document.addEventListener("DOMContentLoaded", () => { input.focus(); return; } + Storage.savePlayerName(name); Storage.clearGameState(); location.href = "game.html"; }); + // Clear validation state on every keystroke input.addEventListener("input", () => { errMsg.textContent = ""; input.classList.remove("input--error"); }); + // Allow form submission via Enter key input.addEventListener("keydown", (e) => { if (e.key === "Enter") btn.click(); }); - // Pre-fill if returning player + // Pre-fill username if the player has played before const existing = Storage.getPlayerName(); if (existing && existing !== "Anonymous") { input.value = existing; diff --git a/frontend/scripts/scoring.js b/frontend/scripts/scoring.js index 32c9f82..b4fa2f5 100644 --- a/frontend/scripts/scoring.js +++ b/frontend/scripts/scoring.js @@ -3,38 +3,44 @@ const Scoring = (() => { /** - * Calculate score from drawn path points. - * MVP: fake score based on number of points drawn (effort-based). - * TODO: replace with real compareShapes() using polygon overlap. + * Calculate a score from the drawn path points. + * NOTE: Currently uses an effort-based approximation. + * TODO: Replace with real polygon comparison (IoU or Hausdorff distance). + * @param {{ x: number, y: number }[]} drawnPoints + * @returns {number} Score between 0 and 100. */ function calculateScore(drawnPoints) { if (!drawnPoints || drawnPoints.length < 10) return 0; - // Fake scoring: reward effort + randomness so it feels real - const effort = Math.min(drawnPoints.length / 300, 1); // 0–1 - const base = 40 + Math.round(effort * 45); // 40–85 - const jitter = Math.round((Math.random() - 0.5) * 14); // ±7 - const score = Math.max(0, Math.min(100, base + jitter)); - return score; + const effort = Math.min(drawnPoints.length / 300, 1); // 0–1 + const base = 40 + Math.round(effort * 45); // 40–85 + const jitter = Math.round((Math.random() - 0.5) * 14); // ±7 + return Math.max(0, Math.min(100, base + jitter)); } /** - * Stub for real shape comparison (future). - * drawnPoints: [{x,y}, ...] - * referencePolygon: [{x,y}, ...] (normalised 0–1 coords) + * Compare a drawn polygon against a reference polygon. + * Stub — reserved for future IoU / Hausdorff implementation. + * @param {{ x: number, y: number }[]} _drawnPoints + * @param {{ x: number, y: number }[]} _referencePolygon - Normalised 0–1 coords. + * @returns {number} Score between 0 and 100. */ function compareShapes(_drawnPoints, _referencePolygon) { - // TODO: implement IoU (Intersection over Union) or - // Hausdorff distance for polygon comparison. + // TODO: implement real shape comparison return 0; } + /** + * Map a numeric score to a letter grade with colour. + * @param {number} score + * @returns {{ label: string, color: string }} + */ function getGrade(score) { if (score >= 90) return { label: "S", color: "#f0b429" }; if (score >= 75) return { label: "A", color: "#41b869" }; if (score >= 60) return { label: "B", color: "#1a7fc4" }; if (score >= 40) return { label: "C", color: "#7a9aaa" }; - return { label: "D", color: "#e05c5c" }; + return { label: "D", color: "#e05c5c" }; } return { calculateScore, compareShapes, getGrade }; diff --git a/frontend/scripts/storage.js b/frontend/scripts/storage.js index 02d42b3..a88409d 100644 --- a/frontend/scripts/storage.js +++ b/frontend/scripts/storage.js @@ -2,32 +2,46 @@ const Storage = (() => { const KEYS = { - PLAYER_NAME: "gd_playerName", - LOBBY_NAME: "gd_lobbyName", - GAME_STATE: "gd_gameState", - LEADERBOARD: "gd_leaderboard", + PLAYER_NAME: "gd_playerName", + LOBBY_NAME: "gd_lobbyName", + GAME_STATE: "gd_gameState", + LEADERBOARD: "gd_leaderboard", }; - function savePlayerName(name) { - localStorage.setItem(KEYS.PLAYER_NAME, name.trim()); + /** + * Safely write a value to localStorage. + * Silently fails if storage quota is exceeded or unavailable. + * @param {string} key + * @param {string} value + */ + function _set(key, value) { + try { + localStorage.setItem(key, value); + } catch { + console.warn(`Storage: could not write key "${key}"`); + } } - function getPlayerName() { - return localStorage.getItem(KEYS.PLAYER_NAME) || "Anonymous"; - } + /** @param {string} name */ + function savePlayerName(name) { _set(KEYS.PLAYER_NAME, name.trim()); } - function saveLobbyName(name) { - localStorage.setItem(KEYS.LOBBY_NAME, name.trim()); - } + /** @returns {string} */ + function getPlayerName() { return localStorage.getItem(KEYS.PLAYER_NAME) || "Anonymous"; } - function getLobbyName() { - return localStorage.getItem(KEYS.LOBBY_NAME) || "My Lobby"; - } + /** @param {string} name */ + function saveLobbyName(name) { _set(KEYS.LOBBY_NAME, name.trim()); } - function saveGameState(state) { - localStorage.setItem(KEYS.GAME_STATE, JSON.stringify(state)); - } + /** @returns {string} */ + function getLobbyName() { return localStorage.getItem(KEYS.LOBBY_NAME) || "My Lobby"; } + /** + * @param {{ scores: number[], totalScore: number, countries: string[] }} state + */ + function saveGameState(state) { _set(KEYS.GAME_STATE, JSON.stringify(state)); } + + /** + * @returns {{ scores: number[], totalScore: number, countries: string[] } | null} + */ function getGameState() { try { return JSON.parse(localStorage.getItem(KEYS.GAME_STATE)) || null; @@ -36,18 +50,22 @@ const Storage = (() => { } } - function clearGameState() { - localStorage.removeItem(KEYS.GAME_STATE); - } + function clearGameState() { localStorage.removeItem(KEYS.GAME_STATE); } + /** + * Add an entry to the leaderboard and keep the top 20. + * @param {{ name: string, totalScore: number, scores: number[], date: string }} entry + */ function saveLeaderboard(entry) { const board = getLeaderboard(); board.push(entry); board.sort((a, b) => b.totalScore - a.totalScore); - const top20 = board.slice(0, 20); - localStorage.setItem(KEYS.LEADERBOARD, JSON.stringify(top20)); + _set(KEYS.LEADERBOARD, JSON.stringify(board.slice(0, 20))); } + /** + * @returns {{ name: string, totalScore: number, scores: number[], date: string }[]} + */ function getLeaderboard() { try { return JSON.parse(localStorage.getItem(KEYS.LEADERBOARD)) || []; @@ -56,20 +74,12 @@ const Storage = (() => { } } - function clearLeaderboard() { - localStorage.removeItem(KEYS.LEADERBOARD); - } + function clearLeaderboard() { localStorage.removeItem(KEYS.LEADERBOARD); } return { - savePlayerName, - getPlayerName, - saveLobbyName, - getLobbyName, - saveGameState, - getGameState, - clearGameState, - saveLeaderboard, - getLeaderboard, - clearLeaderboard, + savePlayerName, getPlayerName, + saveLobbyName, getLobbyName, + saveGameState, getGameState, clearGameState, + saveLeaderboard, getLeaderboard, clearLeaderboard, }; })(); diff --git a/frontend/styles/game.css b/frontend/styles/game.css index 8cf6f72..bd4971f 100644 --- a/frontend/styles/game.css +++ b/frontend/styles/game.css @@ -112,6 +112,15 @@ transition: width 1s linear, background .4s ease; } +/* Timer urgency states — applied via JS classList */ +.timer--warning .timer-bar, +.timer--warning .timer-num { color: var(--gold); } +.timer--warning .timer-bar { background: var(--gold); } + +.timer--danger .timer-bar, +.timer--danger .timer-num { color: var(--danger); } +.timer--danger .timer-bar { background: var(--danger); } + /* ── Canvas area */ .canvas-area { display: grid; From 858ac34ec922614c6e25d4a42fec7af2af137fb5 Mon Sep 17 00:00:00 2001 From: pengniklas Date: Fri, 22 May 2026 16:57:23 +0200 Subject: [PATCH 4/4] accessibility improvements and CSS optimizations --- frontend/game.html | 11 +- frontend/index.html | 11 +- frontend/leaderboard.html | 7 +- frontend/lobby.html | 9 +- frontend/results.html | 7 +- frontend/scripts/countries.js | 244 ++++++------- frontend/scripts/drawing.js | 286 +++++++-------- frontend/scripts/game.js | 181 +++++----- frontend/scripts/index.js | 36 +- frontend/scripts/leaderboard.js | 93 ++--- frontend/scripts/lobby.js | 88 ++--- frontend/scripts/results.js | 101 +++--- frontend/scripts/scoring.js | 77 ++--- frontend/scripts/storage.js | 158 +++++---- frontend/styles/game.css | 388 ++++++++++++--------- frontend/styles/index.css | 593 +++++++++++++++++++------------- frontend/styles/leader.css | 282 +++++++++------ frontend/styles/lobby.css | 216 ++++++------ frontend/styles/main.css | 433 ++++++++++++++--------- frontend/styles/results.css | 256 ++++++++------ 20 files changed, 1976 insertions(+), 1501 deletions(-) diff --git a/frontend/game.html b/frontend/game.html index f95b44f..9aaec39 100644 --- a/frontend/game.html +++ b/frontend/game.html @@ -8,13 +8,16 @@ +
@@ -63,7 +66,7 @@
- +
@@ -105,7 +108,7 @@
diff --git a/frontend/leaderboard.html b/frontend/leaderboard.html index 8897e84..c3a491c 100644 --- a/frontend/leaderboard.html +++ b/frontend/leaderboard.html @@ -9,13 +9,16 @@ +
-