diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index ad17578..fbb0f29 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -226,7 +226,8 @@ def build_save_point_query(): @main_bp.route("/") def home(): - return render_template("index.html") + initial_language = request.accept_languages.best_match(["de", "en"]) or "de" + return render_template("index.html", initial_language=initial_language) @main_bp.route("/dashboard") diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index b83c8e2..a1a6404 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -1,66 +1,224 @@ +:root { + --bg: #f4efe7; + --bg-accent: #efe3cd; + --surface: rgba(255, 250, 242, 0.88); + --surface-strong: #fffaf2; + --surface-soft: rgba(255, 247, 235, 0.76); + --text: #1e1f1d; + --muted: #5f615c; + --navy: #1f3140; + --navy-soft: #324d63; + --gold: #c68b2c; + --gold-soft: #e7bf77; + --gold-pale: #f1ddae; + --line: rgba(31, 49, 64, 0.12); + --shadow: 0 24px 60px rgba(37, 42, 48, 0.14); + --success-color: #166534; + --info-color: #1d4ed8; + --error-color: #991b1b; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { margin: 0; - font-family: Arial, sans-serif; - background: #f5f7fa; + min-height: 100vh; + color: var(--text); + font-family: Georgia, "Times New Roman", serif; + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 24%), + radial-gradient(circle at 82% 18%, rgba(198, 139, 44, 0.2), transparent 20%), + linear-gradient(180deg, #f7f0e5 0%, #efe5d5 52%, #f4efe7 100%); } -/* 🔝 Navbar */ -.navbar { - display: flex; - justify-content: space-between; - align-items: center; - background: #1e293b; - color: white; - padding: 15px 30px; -} - -.navbar a { - color: white; - margin-left: 15px; +a { + color: inherit; text-decoration: none; } -.navbar a:hover { - text-decoration: underline; +button, +input, +select, +textarea { + font: inherit; } -.nav-right { +.dashboard-nav { + width: 100%; + margin: 0; + padding: 18px max(16px, calc((100vw - 1200px) / 2)) 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + position: sticky; + top: 0; + z-index: 20; + border-top: 0; + border-right: 4px solid #12212d; + border-bottom: 4px solid #12212d; + border-left: 4px solid #12212d; + background: linear-gradient(180deg, rgba(17, 29, 39, 0.96), rgba(31, 49, 64, 0.94)); + backdrop-filter: blur(12px); + box-shadow: 0 18px 40px rgba(31, 49, 64, 0.18); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: #fff7eb; + font-size: 18px; + font-weight: 700; + letter-spacing: 0.03em; +} + +.brand-mark { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 8px; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2c2215; + font-size: 22px; +} + +.nav-actions { display: flex; align-items: center; flex-wrap: wrap; + justify-content: flex-end; + gap: 12px; +} + +.nav-link, +.nav-link-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 11px 16px; + border: 2px solid transparent; + background: rgba(255, 255, 255, 0.06); + color: #ecf4fb; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +.lang-switch { + min-width: 68px; +} + +.nav-link:hover, +.nav-link-button:hover { + transform: translateY(-2px); + border-color: rgba(231, 191, 119, 0.45); } .nav-logout-form { - margin: 0 0 0 15px; + margin: 0; } -.nav-link-button { - padding: 0; - border: none; - background: none; - color: white; - font: inherit; +.account-shortcut { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 46px; + padding: 8px 12px 8px 8px; + background: rgba(255, 255, 255, 0.08); + border: 2px solid rgba(231, 191, 119, 0.26); + color: #fff7eb; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.account-shortcut:hover { + transform: translateY(-2px); + border-color: rgba(231, 191, 119, 0.48); +} + +.account-avatar { + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 999px; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2d2418; + font-weight: 700; +} + +.account-copy { + display: grid; + gap: 2px; +} + +.account-label { + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.account-name { + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-size: 14px; +} + +.settings-trigger { + position: fixed; + left: 0; + top: 180px; + z-index: 18; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + border: 0; + border-left: 0; + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #fff7eb; + box-shadow: 0 16px 34px rgba(31, 49, 64, 0.18); cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; } -.nav-link-button:hover { - text-decoration: underline; +.settings-trigger:hover { + transform: translateX(4px); +} + +.settings-trigger-arrow { + font-size: 20px; + line-height: 1; +} + +.settings-trigger-label { + font-size: 14px; + font-weight: 700; + letter-spacing: 0.04em; } -/* 📦 Layout */ .container { - max-width: 900px; - margin: 30px auto; - padding: 10px; + width: min(1200px, calc(100vw - 32px)); + margin: 0 auto; + padding: 34px 0 56px; } -/* 📇 Cards */ .card { - background: white; - padding: 20px; - margin-bottom: 25px; + padding: 28px; + margin-bottom: 24px; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + background: linear-gradient(135deg, rgba(255, 250, 242, 0.94), rgba(245, 230, 205, 0.8)); + border: 1px solid var(--line); + box-shadow: var(--shadow); } .flash-stack { @@ -72,22 +230,129 @@ body { .flash-message { margin: 0; padding: 12px 14px; - border-radius: 10px; + border-radius: 8px; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; } .flash-message.success { - color: #166534; - background: #dcfce7; + color: var(--success-color); + background: rgba(220, 252, 231, 0.92); } .flash-message.info { - color: #1d4ed8; - background: #dbeafe; + color: var(--info-color); + background: rgba(219, 234, 254, 0.92); } .flash-message.error { - color: #b91c1c; - background: #fee2e2; + color: var(--error-color); + background: rgba(254, 226, 226, 0.92); +} + +.section-kicker { + margin: 0 0 12px; + color: var(--navy-soft); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 13px; + font-weight: 700; +} + +.dashboard-intro { + text-align: center; +} + +.dashboard-intro h1 { + margin: 0; + line-height: 0.96; + letter-spacing: -0.03em; + font-size: clamp(2.4rem, 4.4vw, 4.8rem); + color: #182029; +} + +.dashboard-intro-text, +.board-mode-text, +.solver-message, +.solver-group-text, +.history-subtitle, +.board-size-info, +.sidebar p, +.sidebar-hint, +.modal-content p, +.save-point-meta, +.save-point-preview, +.save-point-note { + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + line-height: 1.65; + color: var(--muted); +} + +.dashboard-intro-text { + max-width: 760px; + margin: 18px auto 0; + font-size: 18px; +} + +.btn { + min-height: 50px; + padding: 12px 18px; + border: none; + border-radius: 0; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2d2418; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 12px 24px rgba(31, 49, 64, 0.16); +} + +.btn:active { + transform: scale(0.98); +} + +.btn:disabled, +.btn:disabled:hover { + transform: none; + opacity: 0.55; + box-shadow: none; + cursor: not-allowed; +} + +.btn.secondary { + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #f9fbfd; +} + +.btn.danger, +.btn.ghost { + background: rgba(255, 255, 255, 0.68); + color: var(--navy); + border: 1px solid rgba(31, 49, 64, 0.16); +} + +.btn.active, +.btn.accent { + background: linear-gradient(135deg, var(--gold), #f0c35b); + color: #241b0d; +} + +.mode-switch { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 16px; + margin-top: 28px; +} + +.mode-switch .btn { + min-width: 220px; + min-height: 68px; + padding: 18px 28px; + font-size: 1.15rem; } .game-card { @@ -96,70 +361,125 @@ body { .game-card-header { display: flex; - align-items: center; - justify-content: center; - gap: 12px; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + text-align: left; } -.game-card-header h2 { +.game-card-copy { + max-width: 760px; +} + +.game-card h2, +.card h3, +.solver-panel h3, +.sidebar h3, +.modal-header h3 { margin: 0; + color: #182029; +} + +.game-card h2 { + line-height: 0.98; + letter-spacing: -0.03em; + font-size: clamp(2rem, 3.2vw, 3.2rem); +} + +.board-mode-text { + margin: 16px 0 0; + font-size: 18px; } .info-button { - width: 38px; - height: 38px; - border: none; - border-radius: 999px; - background: #dbeafe; - color: #1d4ed8; - font-size: 20px; - font-weight: bold; + min-height: 48px; + padding: 12px 18px; + border: 1px solid rgba(31, 49, 64, 0.14); + background: rgba(255, 255, 255, 0.66); + color: var(--navy); + font-weight: 700; cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; } .info-button:hover { - background: #bfdbfe; + transform: translateY(-2px); + box-shadow: 0 12px 24px rgba(31, 49, 64, 0.12); +} + +.board-size-info { + margin: 18px 0 10px; + font-size: 16px; } .game-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin: 18px 0 12px; + gap: 14px; + margin: 18px 0 14px; + text-align: left; } .game-stat-card { - padding: 14px 16px; - border-radius: 12px; - background: #f8fafc; - border: 1px solid #dbe4ee; - text-align: left; + padding: 16px 18px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.56); + border: 1px solid rgba(31, 49, 64, 0.08); } .game-stat-label { margin: 0 0 8px; - font-size: 14px; - color: #64748b; + font-size: 13px; + color: var(--navy-soft); text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.12em; + font-weight: 700; } .game-stat-value { margin: 0; - font-size: 26px; - font-weight: bold; + font-size: 28px; + font-weight: 700; color: #1e293b; font-variant-numeric: tabular-nums; } .game-stat-status { - font-size: 20px; + font-size: 22px; } #statusMessage { min-height: 24px; - margin: 0 0 8px; - font-weight: bold; + margin: 0 0 10px; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-weight: 700; +} + +.solved-celebration { + margin: 0 auto 18px; + padding: 18px 20px; + max-width: 760px; + border: 1px solid rgba(198, 139, 44, 0.28); + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.75), transparent 35%), + linear-gradient(135deg, rgba(240, 195, 91, 0.96), rgba(231, 191, 119, 0.9)); + color: #2d2418; + box-shadow: 0 18px 40px rgba(198, 139, 44, 0.18); + animation: solvedPop 0.55s ease; +} + +.solved-celebration-title { + margin: 0; + font-size: clamp(1.8rem, 3vw, 2.6rem); + font-weight: 700; + letter-spacing: -0.03em; +} + +.solved-celebration-text { + margin: 8px 0 0; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-size: 16px; + line-height: 1.55; } .board { @@ -167,16 +487,16 @@ body { padding-bottom: 8px; } -/* ♟️ Board */ .board-grid { - --label-size: 30px; + --label-size: var(--cell-size); --cell-size: 60px; display: grid; grid-template-columns: var(--label-size) repeat(var(--board-size, 8), var(--cell-size)); grid-template-rows: var(--label-size) repeat(var(--board-size, 8), var(--cell-size)); margin: 20px auto; width: max-content; - border: 6px solid #1e293b; + background: var(--navy); + border: 6px solid var(--navy); border-radius: 10px; overflow: hidden; max-width: 100%; @@ -186,12 +506,13 @@ body { display: flex; align-items: center; justify-content: center; - font-weight: bold; - background: #aec8eb; - color: #1e293b; + font-weight: 700; + background: var(--navy); + color: #fff7eb; width: var(--label-size); height: var(--label-size); - font-size: 14px; + font-size: clamp(13px, 1.4vw, 18px); + text-align: center; } .cell { @@ -202,116 +523,32 @@ body { justify-content: center; font-size: calc(var(--cell-size) * 0.5); cursor: pointer; - transition: 0.2s; + transition: transform 0.2s ease, background 0.2s ease; } .cell:hover { transform: scale(1.05); } -.cell.solver-place { - box-shadow: inset 0 0 0 4px #22c55e; - animation: solverPulse 0.35s ease; -} - -.cell.solver-remove { - box-shadow: inset 0 0 0 4px #f59e0b; - animation: solverPulse 0.35s ease; -} - -.cell.solver-solution { - box-shadow: inset 0 0 0 4px #2563eb; - animation: solverPulse 0.35s ease; -} - .white { - background: #e2e8f0; + background: #f7e6ca; } .black { - background: #475569; - color: white; + background: #92613a; + color: #fff5ea; } .invalid { - background: #ef4444 !important; -} - -/* 🎛️ Buttons */ -.controls { - margin-top: 15px; - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 10px; -} - -.btn { - padding: 10px 15px; - margin: 5px; - border: none; - border-radius: 8px; - background: #3b82f6; - color: white; - cursor: pointer; -} - -.btn:hover { - background: #2563eb; -} - -.btn:disabled, -.btn:disabled:hover { - background: #94a3b8; - cursor: not-allowed; -} - -.btn.secondary { - background: #64748b; -} - -.btn.secondary:hover { - background: #475569; -} - -.btn.danger { - background: #ef4444; -} - -.btn.danger:hover { - background: #dc2626; -} - -#queenList { - list-style: none; - padding: 0; -} - -#queenList li { - padding: 5px; - background: #f1f5f9; - margin: 4px 0; - border-radius: 6px; -} - -.mode-switch { - margin-bottom: 15px; -} - -.mode-switch .btn { - margin: 5px; -} - -.btn.active { - background: #22c55e; + background: #d65555 !important; } .solver-panel { - margin-top: 12px; - padding: 16px; + margin-top: 18px; + padding: 18px; border-radius: 12px; - border: 1px solid #dbe4ee; - background: #f8fafc; + border: 1px solid rgba(31, 49, 64, 0.1); + background: rgba(255, 250, 242, 0.74); text-align: left; } @@ -323,36 +560,61 @@ body { } .solver-panel h3 { - margin: 0 0 6px; - color: #1e293b; + margin-bottom: 8px; } .solver-message { margin: 0; - color: #475569; } .solver-step-info { margin: 0; - padding: 6px 10px; - border-radius: 999px; - background: #dbeafe; - color: #1d4ed8; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.74); + border: 1px solid rgba(31, 49, 64, 0.12); + color: var(--navy); font-size: 14px; - font-weight: bold; + font-weight: 700; white-space: nowrap; } -.solver-controls { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 14px; +.solver-groups { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr); + gap: 16px; + margin-top: 18px; } -.solver-controls .btn, -.controls .btn { +.solver-group { + padding: 18px; + border: 1px solid rgba(31, 49, 64, 0.1); + background: rgba(255, 255, 255, 0.58); +} + +.solver-group h4 { + margin: 0 0 8px; + color: #182029; + font-size: 1.15rem; +} + +.solver-group-text { margin: 0; + font-size: 15px; +} + +.solver-controls { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 16px; +} + +.instant-solver-actions { + margin-top: 16px; +} + +.instant-solver-actions .btn { + width: 100%; } .solver-speed { @@ -360,7 +622,7 @@ body { grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; - margin-top: 14px; + margin-top: 16px; } .solver-speed input[type="range"] { @@ -368,31 +630,131 @@ body { } .solver-speed-label { - font-weight: bold; - color: #334155; + font-weight: 700; + color: var(--navy); } .solver-speed-value { min-width: 62px; text-align: right; - color: #475569; + color: var(--muted); font-variant-numeric: tabular-nums; } -.board-size-info { - margin: 0 0 10px; - color: #475569; +.controls { + margin-top: 18px; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 10px; +} + +.controls-save-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + width: min(420px, 100%); +} + +.settings-sidebar-actions { + margin-top: 18px; + display: grid; + gap: 10px; +} + +.section-header, +.history-header, +.sidebar-header, +.modal-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; +} + +.section-badge { + padding: 8px 12px; + background: rgba(255, 255, 255, 0.66); + border: 1px solid rgba(31, 49, 64, 0.1); + color: var(--navy); + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-size: 14px; + font-weight: 700; +} + +#queenList { + list-style: none; + padding: 0; + margin: 18px 0 0; + display: grid; + gap: 10px; +} + +#queenList li { + padding: 12px 14px; + background: rgba(255, 255, 255, 0.56); + border: 1px solid rgba(31, 49, 64, 0.08); + border-radius: 8px; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + color: var(--navy); +} + +#queenList li.empty { + color: var(--muted); +} + +.history-header h3 { + margin-top: 4px; +} + +.history-subtitle { + margin: 8px 0 0; + font-size: 15px; +} + +.history-table-wrapper { + overflow-x: auto; + margin-top: 18px; +} + +.history-table { + width: 100%; + border-collapse: collapse; + min-width: 720px; +} + +.history-table th, +.history-table td { + padding: 12px 10px; + border-bottom: 1px solid rgba(31, 49, 64, 0.08); + text-align: left; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; +} + +.history-table th { + color: var(--navy); + font-size: 14px; +} + +.history-table td { + color: var(--muted); + font-size: 14px; +} + +.history-empty { + color: var(--muted); + text-align: center !important; } .sidebar { position: fixed; top: 0; right: 0; - width: min(320px, 85vw); + width: min(360px, 92vw); height: 100vh; padding: 24px; - box-sizing: border-box; - background: white; + background: linear-gradient(180deg, rgba(255, 250, 242, 0.98), rgba(245, 230, 205, 0.96)); + border-left: 1px solid rgba(31, 49, 64, 0.12); box-shadow: -10px 0 24px rgba(15, 23, 42, 0.18); transform: translateX(100%); transition: transform 0.25s ease; @@ -406,44 +768,45 @@ body { transform: translateX(0); } +.settings-sidebar { + left: 0; + right: auto; + border-left: 0; + border-right: 1px solid rgba(31, 49, 64, 0.12); + box-shadow: 10px 0 24px rgba(15, 23, 42, 0.18); + transform: translateX(-100%); +} + +.settings-sidebar.open { + transform: translateX(0); +} + .save-points-sidebar { - width: min(380px, 92vw); -} - -.save-sidebar { - width: min(360px, 90vw); -} - -.sidebar-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; -} - -.sidebar-close { - margin: 0; + width: min(400px, 94vw); } .sidebar-label { display: block; margin-top: 18px; margin-bottom: 8px; - font-weight: bold; + font-weight: 700; + color: var(--navy); } .sidebar input[type="range"] { width: 100%; } -.sidebar-input { +.sidebar-input, +.sidebar select, +.sidebar textarea, +.sidebar input[type="text"] { width: 100%; - padding: 10px 12px; - border: 1px solid #cbd5e1; - border-radius: 8px; - font: inherit; - box-sizing: border-box; - background: #fff; + padding: 12px 14px; + border: 1px solid rgba(31, 49, 64, 0.16); + border-radius: 0; + background: rgba(255, 255, 255, 0.9); + color: var(--text); } .sidebar-textarea { @@ -456,7 +819,8 @@ body { align-items: center; gap: 10px; margin: 16px 0 18px; - color: #334155; + color: var(--navy); + font-family: "Trebuchet MS", "Segoe UI", sans-serif; } .sidebar-size-row { @@ -464,11 +828,6 @@ body { font-size: 18px; } -.sidebar-hint { - color: #64748b; - font-size: 14px; -} - .save-points-list { display: grid; gap: 12px; @@ -480,14 +839,13 @@ body { gap: 8px; margin-top: 18px; padding-bottom: 16px; - border-bottom: 1px solid #e2e8f0; + border-bottom: 1px solid rgba(31, 49, 64, 0.08); } .save-point-card { - border: 1px solid #dbe4ee; - border-radius: 10px; + border: 1px solid rgba(31, 49, 64, 0.1); padding: 14px; - background: #f8fafc; + background: rgba(255, 255, 255, 0.56); } .save-point-card h4 { @@ -495,22 +853,10 @@ body { color: #1e293b; } -.save-point-meta { - margin: 4px 0; - color: #475569; - font-size: 14px; -} - -.save-point-preview { - margin: 10px 0 0; - color: #334155; - font-size: 14px; - word-break: break-word; -} - +.save-point-meta, +.save-point-preview, .save-point-note { - margin: 10px 0 0; - color: #475569; + margin: 6px 0 0; font-size: 14px; } @@ -521,83 +867,6 @@ body { margin-top: 14px; } -.save-point-actions .btn { - margin: 0; -} - -.inline-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; -} - -.inline-actions form { - margin: 0; -} - -.inline-actions .btn { - margin: 0; -} - -.history-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - margin-bottom: 14px; -} - -.history-header h3 { - margin: 0; -} - -.history-subtitle { - margin: 6px 0 0; - color: #64748b; -} - -.history-table-wrapper { - overflow-x: auto; -} - -.history-table { - width: 100%; - border-collapse: collapse; - min-width: 720px; -} - -.history-table th, -.history-table td { - padding: 12px 10px; - border-bottom: 1px solid #e2e8f0; - text-align: left; -} - -.history-table th { - color: #334155; - font-size: 14px; -} - -.history-table td { - color: #475569; - font-size: 14px; -} - -.history-empty { - color: #64748b; - text-align: center !important; -} - -.btn.ghost { - background: #e2e8f0; - color: #1e293b; -} - -.btn.ghost:hover { - background: #cbd5e1; -} - .sidebar-backdrop { position: fixed; inset: 0; @@ -632,12 +901,12 @@ body { position: fixed; top: 50%; left: 50%; - width: min(620px, calc(100vw - 32px)); + width: min(680px, calc(100vw - 32px)); max-height: min(82vh, 720px); padding: 24px; - box-sizing: border-box; - background: white; - border-radius: 18px; + background: linear-gradient(180deg, rgba(255, 250, 242, 0.98), rgba(245, 230, 205, 0.96)); + border-radius: 12px; + border: 1px solid rgba(31, 49, 64, 0.12); box-shadow: 0 20px 60px rgba(15, 23, 42, 0.28); transform: translate(-50%, -48%); opacity: 0; @@ -653,31 +922,29 @@ body { transform: translate(-50%, -50%); } -.modal-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; -} - -.modal-header h3 { - margin: 0; -} - .modal-content { margin-top: 16px; - color: #334155; text-align: left; } .modal-content h4 { - margin: 16px 0 8px; - color: #1e293b; + margin: 18px 0 8px; + color: var(--navy); } -.modal-content p { - margin: 0; - line-height: 1.6; +.cell.solver-place { + box-shadow: inset 0 0 0 4px #2f8f53; + animation: solverPulse 0.35s ease; +} + +.cell.solver-remove { + box-shadow: inset 0 0 0 4px #d08a24; + animation: solverPulse 0.35s ease; +} + +.cell.solver-solution { + box-shadow: inset 0 0 0 4px #2563eb; + animation: solverPulse 0.35s ease; } @keyframes solverPulse { @@ -694,38 +961,128 @@ body { } } -@media (max-width: 720px) { - .container { - padding: 12px; +@keyframes solvedPop { + 0% { + opacity: 0; + transform: translateY(-10px) scale(0.96); } - .navbar { - padding: 15px 18px; + 60% { + opacity: 1; + transform: translateY(2px) scale(1.02); } - - .nav-logout-form { - margin-left: 0; - } - - .solver-panel-header, - .solver-speed { - grid-template-columns: 1fr; - display: grid; - } - - .game-stats { - grid-template-columns: 1fr; - } - - .history-header, - .modal-header { - grid-template-columns: 1fr; - display: grid; - } - - .solver-step-info, - .solver-speed-value { - justify-self: start; + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (max-width: 980px) { + .solver-groups { + grid-template-columns: 1fr; + } + + .solver-controls { + grid-template-columns: 1fr; + } + + .game-card-header, + .history-header, + .section-header { + flex-direction: column; + } +} + +@media (max-width: 760px) { + .dashboard-nav { + position: static; + padding: 18px 16px; + border-left-width: 0; + border-right-width: 0; + flex-direction: column; + align-items: flex-start; + } + + .brand { + width: 100%; + font-size: 16px; + } + + .nav-actions { + width: 100%; + justify-content: flex-start; + } + + .account-name { + display: none; + } + + .settings-trigger { + top: auto; + bottom: 16px; + left: 12px; + max-width: calc(100vw - 24px); + } + + .container { + width: 100%; + padding-top: 22px; + } + + .card { + padding: 22px 18px; + } + + .game-card { + padding-left: 8px; + padding-right: 8px; + } + + .board { + margin-left: -4px; + margin-right: -4px; + padding-bottom: 4px; + } + + .board-grid { + margin: 12px auto; + border-width: 4px; + border-radius: 8px; + } + + .label { + font-size: 12px; + } + + .game-stats, + .solver-speed { + grid-template-columns: 1fr; + } + + .mode-switch .btn { + min-width: min(100%, 280px); + } + + .controls { + justify-content: stretch; + } + + .controls>.btn, + .save-point-actions .btn { + width: 100%; + } + + .controls-save-row { + width: 100%; + } + + .sidebar { + width: min(100%, calc(100vw - 24px)); + } + + .modal { + width: min(100%, calc(100vw - 24px)); + padding: 20px; } } diff --git a/app/static/css/index.css b/app/static/css/index.css new file mode 100644 index 0000000..1aaef0c --- /dev/null +++ b/app/static/css/index.css @@ -0,0 +1,514 @@ +:root { + --bg: #f4efe7; + --bg-accent: #efe3cd; + --surface: rgba(255, 250, 242, 0.82); + --surface-strong: #fffaf2; + --text: #1e1f1d; + --muted: #5f615c; + --navy: #1f3140; + --navy-soft: #324d63; + --gold: #c68b2c; + --gold-soft: #e7bf77; + --line: rgba(31, 49, 64, 0.12); + --shadow: 0 24px 60px rgba(37, 42, 48, 0.14); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); + font-family: Georgia, "Times New Roman", serif; + background: + radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 24%), + radial-gradient(circle at 82% 18%, rgba(198, 139, 44, 0.2), transparent 20%), + linear-gradient(180deg, #f7f0e5 0%, #efe5d5 52%, #f4efe7 100%); +} + +a { + color: inherit; + text-decoration: none; +} + +main { + width: min(1200px, calc(100vw - 32px)); + margin: 0 auto; + padding: 34px 0 56px; +} + +.landing-nav { + width: 100%; + margin: 0; + padding: 18px max(16px, calc((100vw - 1200px) / 2)) 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + position: sticky; + top: 0; + z-index: 20; + border-top: 0; + border-right: 4px solid #12212d; + border-bottom: 4px solid #12212d; + border-left: 4px solid #12212d; + background: + linear-gradient(180deg, rgba(17, 29, 39, 0.96), rgba(31, 49, 64, 0.94)); + backdrop-filter: blur(12px); + box-shadow: 0 18px 40px rgba(31, 49, 64, 0.18); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: #fff7eb; + font-size: 18px; + font-weight: 700; + letter-spacing: 0.03em; +} + +.brand-mark { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 8px; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2c2215; + font-size: 22px; +} + +.landing-links { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14px; + color: #ecf4fb; +} + +.landing-links a:not(.nav-cta) { + padding: 12px 14px; + border: 2px solid transparent; + background: rgba(255, 255, 255, 0.04); + opacity: 0.92; +} + +.landing-links a:not(.nav-cta):hover { + opacity: 1; + border-color: rgba(231, 191, 119, 0.45); +} + +.lang-switch, +.nav-cta, +.hero-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 13px 20px; + font-weight: 700; + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.lang-switch { + min-width: 68px; + border: 2px solid rgba(231, 191, 119, 0.45); + background: rgba(255, 255, 255, 0.06); + color: #fff7eb; + cursor: pointer; + font-family: inherit; +} + +.nav-cta { + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2d2418; +} + +.nav-cta.secondary { + background: rgba(255, 255, 255, 0.08); + color: #f8fafc; + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.lang-switch:hover, +.nav-cta:hover, +.hero-button:hover { + transform: translateY(-2px); +} + +.hero { + padding: 88px 0 56px; + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); + gap: 42px; + align-items: center; +} + +.eyebrow { + margin: 0 0 12px; + color: var(--navy-soft); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 13px; + font-weight: 700; +} + +.hero h1, +.info-band h2, +.section-heading h2, +.cta-panel h2 { + margin: 0; + line-height: 0.95; + letter-spacing: -0.03em; + font-size: clamp(2.8rem, 5vw, 5.5rem); + color: #182029; +} + +.hero-text, +.info-band p, +.feature-card p, +.journey-step p, +.cta-panel p { + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-size: 18px; + line-height: 1.7; + color: var(--muted); +} + +.hero-text { + max-width: 640px; + margin: 24px 0 0; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 30px; +} + +.hero-button.primary { + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #f9fbfd; + box-shadow: 0 14px 30px rgba(31, 49, 64, 0.22); +} + +.hero-button.secondary { + border: 1px solid rgba(31, 49, 64, 0.16); + background: rgba(255, 255, 255, 0.5); + color: var(--navy); +} + +.hero-facts { + margin-top: 30px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.hero-facts article { + padding: 18px; + border-radius: 10px; + background: rgba(255, 250, 242, 0.65); + border: 1px solid rgba(31, 49, 64, 0.08); + box-shadow: var(--shadow); +} + +.hero-facts strong { + display: block; + font-size: 18px; + color: var(--navy); +} + +.hero-facts span { + display: block; + margin-top: 6px; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + color: var(--muted); + line-height: 1.5; +} + +.hero-visual { + position: relative; + min-height: 620px; + display: grid; + place-items: center; +} + +.board-showcase { + width: min(100%, 520px); + aspect-ratio: 1; + padding: 18px; + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 0; + border-radius: 14px; + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.85), rgba(247, 233, 210, 0.7)), + rgba(255, 255, 255, 0.5); + box-shadow: var(--shadow); + position: relative; + overflow: hidden; +} + +.board-cell { + position: relative; + display: grid; + place-items: center; + font-size: clamp(20px, 2vw, 30px); + transition: transform 0.45s ease, opacity 0.45s ease, background 0.45s ease; +} + +.board-cell.light { + background: #f7e6ca; +} + +.board-cell.dark { + background: #92613a; +} + +.board-cell.highlight { + background: #2f485f; +} + +.board-cell.queen::after { + content: "♛"; + color: #fff3df; + text-shadow: 0 8px 18px rgba(0, 0, 0, 0.28); + transform: scale(1); + animation: queenPulse 1.8s ease-in-out infinite; +} + +.floating-card { + position: absolute; + padding: 16px 18px; + border-radius: 8px; + background: rgba(255, 250, 242, 0.85); + border: 1px solid rgba(31, 49, 64, 0.08); + box-shadow: 0 18px 35px rgba(31, 49, 64, 0.14); + backdrop-filter: blur(10px); +} + +.solver-card { + top: 26px; + right: -8px; +} + +.timer-card { + left: -8px; + bottom: 34px; +} + +.floating-label { + margin: 0; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--muted); +} + +.floating-value { + margin: 6px 0 0; + font-size: 22px; + color: var(--navy); +} + +.info-band, +.feature-section, +.journey-section, +.cta-section { + padding: 38px 0; +} + +.info-band-inner, +.cta-panel { + padding: 36px; + border-radius: 12px; + background: linear-gradient(135deg, rgba(255, 250, 242, 0.88), rgba(245, 230, 205, 0.7)); + border: 1px solid rgba(31, 49, 64, 0.08); + box-shadow: var(--shadow); +} + +.info-band h2 { + font-size: clamp(2.2rem, 4vw, 4.2rem); + margin-bottom: 18px; +} + +.section-heading { + max-width: 760px; + margin-bottom: 28px; +} + +.section-heading h2 { + font-size: clamp(2.2rem, 3.8vw, 4rem); +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.feature-card, +.journey-step { + padding: 26px; + border-radius: 10px; + background: rgba(255, 250, 242, 0.72); + border: 1px solid rgba(31, 49, 64, 0.08); + box-shadow: var(--shadow); +} + +.feature-icon { + display: inline-grid; + place-items: center; + width: 54px; + height: 54px; + border-radius: 8px; + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #fff3df; + font-size: 28px; +} + +.feature-card h3, +.journey-step h3 { + margin: 18px 0 10px; + font-size: 28px; + color: #1c2833; +} + +.journey-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} + +.step-number { + display: inline-block; + margin-bottom: 18px; + color: var(--gold); + font-size: 14px; + font-weight: 700; + letter-spacing: 0.16em; +} + +.cta-panel { + text-align: center; +} + +.cta-panel h2 { + font-size: clamp(2.2rem, 4vw, 4.4rem); +} + +.cta-panel p { + max-width: 760px; + margin: 18px auto 0; +} + +.cta-panel .hero-actions { + justify-content: center; +} + +@keyframes queenPulse { + 0%, + 100% { + transform: scale(1) translateY(0); + } + + 50% { + transform: scale(1.08) translateY(-2px); + } +} + +@media (max-width: 980px) { + .hero { + grid-template-columns: 1fr; + gap: 28px; + padding-top: 54px; + } + + .hero-visual { + min-height: auto; + } + + .journey-grid, + .feature-grid, + .hero-facts { + grid-template-columns: 1fr; + } + + .solver-card, + .timer-card { + position: static; + } + + .hero-visual { + gap: 16px; + } +} + +@media (max-width: 760px) { + .landing-nav { + position: static; + width: 100%; + padding: 18px 16px; + border-left-width: 0; + border-right-width: 0; + gap: 14px; + align-items: flex-start; + flex-direction: column; + } + + .brand { + width: 100%; + justify-content: space-between; + gap: 8px; + font-size: 16px; + } + + .brand-mark { + display: none; + } + + .landing-links { + justify-content: flex-start; + gap: 10px; + width: 100%; + } + + .landing-links a:not(.nav-cta), + .lang-switch, + .nav-cta { + padding: 11px 14px; + } + + main { + width: min(100%, calc(100vw - 24px)); + } + + .hero h1, + .info-band h2, + .section-heading h2, + .cta-panel h2 { + line-height: 1.02; + } + + .info-band-inner, + .cta-panel, + .feature-card, + .journey-step { + padding: 24px; + } + + .floating-card { + width: 100%; + } +} diff --git a/app/static/css/login.css b/app/static/css/login.css index 21554a9..d4a1008 100644 --- a/app/static/css/login.css +++ b/app/static/css/login.css @@ -1,17 +1,21 @@ :root { - --nav-bg-color: #172033; - --panel-bg-color: rgba(255, 255, 255, 0.18); - --panel-border-color: rgba(255, 255, 255, 0.24); - --text-color: #172033; - --muted-text-color: #475569; - --success-color: #15803d; + --bg: #f4efe7; + --bg-accent: #efe3cd; + --surface: rgba(255, 250, 242, 0.88); + --surface-strong: #fffaf2; + --surface-soft: rgba(255, 247, 235, 0.72); + --text-color: #1e1f1d; + --muted-text-color: #5f615c; + --navy: #1f3140; + --navy-soft: #324d63; + --gold: #c68b2c; + --gold-soft: #e7bf77; + --gold-pale: #f1ddae; + --line: rgba(31, 49, 64, 0.12); + --shadow: 0 24px 60px rgba(37, 42, 48, 0.14); + --success-color: #166534; --info-color: #1d4ed8; - --error-color: #b91c1c; -} - -@font-face { - font-family: "Geist Sans"; - src: url("https://assets.codepen.io/605876/GeistVF.ttf") format("truetype"); + --error-color: #991b1b; } *, @@ -24,74 +28,103 @@ body { margin: 0; min-height: 100vh; color: var(--text-color); - font-family: "Geist Sans", system-ui, sans-serif; + font-family: Georgia, "Times New Roman", serif; background: - radial-gradient(circle at top left, rgba(255, 255, 255, 0.45), transparent 32%), - linear-gradient(135deg, #d2ecff 0%, #b8c6db 48%, #7f8ea3 100%); + radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 24%), + radial-gradient(circle at 82% 18%, rgba(198, 139, 44, 0.2), transparent 20%), + linear-gradient(180deg, #f7f0e5 0%, #efe5d5 52%, #f4efe7 100%); } -nav { +a { + color: inherit; + text-decoration: none; +} + +.auth-nav { width: 100%; - min-height: 68px; - padding: 15px 30px; + margin: 0; + padding: 18px max(16px, calc((100vw - 1200px) / 2)) 16px; display: flex; align-items: center; justify-content: space-between; - gap: 20px; - background: var(--nav-bg-color); + gap: 18px; + position: sticky; + top: 0; + z-index: 10; + border-top: 0; + border-right: 4px solid #12212d; + border-bottom: 4px solid #12212d; + border-left: 4px solid #12212d; + background: linear-gradient(180deg, rgba(17, 29, 39, 0.96), rgba(31, 49, 64, 0.94)); + backdrop-filter: blur(12px); + box-shadow: 0 18px 40px rgba(31, 49, 64, 0.18); } -nav ul { +.brand { + display: inline-flex; + align-items: center; + gap: 12px; + color: #fff7eb; + font-size: 18px; + font-weight: 700; + letter-spacing: 0.03em; +} + +.brand-mark { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 8px; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2c2215; + font-size: 22px; +} + +.nav-links { margin: 0; padding: 0; list-style: none; display: flex; + align-items: center; flex-wrap: wrap; - gap: 20px; + gap: 12px; + color: #ecf4fb; } -nav a { - color: white; - text-decoration: none; - font-size: 16px; +.nav-links a, +.nav-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 11px 16px; + border: 2px solid transparent; + background: rgba(255, 255, 255, 0.06); + color: inherit; + font: inherit; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; } -nav a:hover { - color: #93c5fd; +.nav-links a:hover, +.nav-button:hover { + transform: translateY(-2px); + border-color: rgba(231, 191, 119, 0.45); + color: #fff7eb; } .nav-form { margin: 0; - padding: 0; - width: auto; - background: none; - border: 0; - box-shadow: none; - backdrop-filter: none; -} - -.nav-button { - margin: 0; - padding: 0; - background: none; - border: 0; - color: white; - font: inherit; - cursor: pointer; -} - -.nav-button:hover { - transform: none; - box-shadow: none; - color: #93c5fd; } .auth-shell { - min-height: calc(100vh - 68px); + min-height: calc(100vh - 78px); display: flex; align-items: center; justify-content: center; - padding: 32px 16px; + padding: 38px 16px 48px; } .settings-shell { @@ -99,18 +132,17 @@ nav a:hover { } .auth-panel { - width: min(420px, 100%); + width: min(460px, 100%); padding: 34px; - border-radius: 20px; - background: var(--panel-bg-color); - border: 1px solid var(--panel-border-color); - backdrop-filter: blur(16px); - box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18); + border-radius: 12px; + background: linear-gradient(135deg, rgba(255, 250, 242, 0.94), rgba(245, 230, 205, 0.8)); + border: 1px solid var(--line); + box-shadow: var(--shadow); animation: fadeIn 0.8s ease; } .settings-panel { - width: min(900px, 100%); + width: min(980px, 100%); } .settings-grid { @@ -134,12 +166,31 @@ nav a:hover { h1, h2 { margin-top: 0; + color: #182029; +} + +h1 { + margin-bottom: 10px; + line-height: 0.98; + letter-spacing: -0.03em; + font-size: clamp(2.2rem, 4vw, 3.6rem); +} + +h2 { + margin-bottom: 10px; + font-size: 1.55rem; +} + +.muted, +.field-hint { + font-family: "Trebuchet MS", "Segoe UI", sans-serif; + line-height: 1.6; + color: var(--muted-text-color); } .muted { margin: 0 0 20px; - color: var(--muted-text-color); - line-height: 1.5; + font-size: 17px; } form { @@ -148,65 +199,56 @@ form { gap: 8px; margin-top: 18px; padding: 24px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 10px; + background: var(--surface-soft); + border: 1px solid rgba(31, 49, 64, 0.1); } label { margin-top: 8px; font-size: 14px; - font-weight: 600; + font-weight: 700; + color: var(--navy); + letter-spacing: 0.03em; } input { width: 100%; padding: 12px 14px; - border: 1px solid rgba(71, 85, 105, 0.25); - border-radius: 10px; + border: 1px solid rgba(31, 49, 64, 0.16); + border-radius: 0; font: inherit; background: rgba(255, 255, 255, 0.86); color: var(--text-color); } input:focus { - outline: 2px solid rgba(59, 130, 246, 0.35); - border-color: rgba(59, 130, 246, 0.5); + outline: 2px solid rgba(198, 139, 44, 0.28); + border-color: rgba(198, 139, 44, 0.55); } .field-hint { margin: 0; font-size: 13px; - color: var(--muted-text-color); -} - -.checkbox-row { - display: flex; - align-items: center; - gap: 10px; - margin: 10px 0 6px; -} - -.checkbox-row input { - width: auto; } button { margin-top: 14px; - padding: 12px 16px; + min-height: 50px; + padding: 12px 18px; border: none; - border-radius: 10px; - background: linear-gradient(135deg, #eff6ff, #dbeafe); - color: #172033; + border-radius: 0; + background: linear-gradient(135deg, var(--gold), var(--gold-soft)); + color: #2d2418; cursor: pointer; font-size: 16px; - font-weight: 600; + font-weight: 700; transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; } button:hover { transform: translateY(-2px); - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18); + box-shadow: 0 12px 24px rgba(31, 49, 64, 0.16); } button:active { @@ -220,18 +262,20 @@ button:active { } .button-row button { - flex: 1 1 160px; + flex: 1 1 180px; } .secondary-button { - background: linear-gradient(135deg, #e2e8f0, #cbd5e1); + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #f9fbfd; } .message { margin: 0 0 14px; padding: 12px 14px; - border-radius: 10px; + border-radius: 8px; font-size: 14px; + font-family: "Trebuchet MS", "Segoe UI", sans-serif; } .message.error { @@ -254,9 +298,9 @@ button:active { justify-content: center; margin: 8px 0 6px; padding: 18px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.38); - border: 1px solid rgba(255, 255, 255, 0.24); + border-radius: 10px; + background: rgba(255, 250, 242, 0.72); + border: 1px solid rgba(31, 49, 64, 0.12); } .qr-code { @@ -266,7 +310,7 @@ button:active { min-width: 220px; min-height: 220px; padding: 10px; - border-radius: 14px; + border-radius: 8px; background: #ffffff; color: var(--muted-text-color); text-align: center; @@ -276,15 +320,32 @@ button:active { display: block; } -@media (max-width: 640px) { - nav { - padding: 16px 20px; +@media (max-width: 760px) { + .auth-nav { + position: static; + padding: 18px 16px; + border-left-width: 0; + border-right-width: 0; flex-direction: column; align-items: flex-start; } + .brand { + width: 100%; + font-size: 16px; + } + + .nav-links { + width: 100%; + } + + .nav-links a, + .nav-button { + padding: 10px 14px; + } + .auth-shell { - padding: 20px 12px 28px; + padding: 22px 12px 28px; } .auth-panel { @@ -294,4 +355,4 @@ button:active { form { padding: 18px; } -} +} \ No newline at end of file diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index 691af6e..0e7ea99 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -2,7 +2,6 @@ const DEFAULT_BOARD_SIZE = 8; const MIN_BOARD_SIZE = 4; const MAX_BOARD_SIZE = 13; const EMPTY_CELL = -1; -const DEFAULT_SOLVER_MESSAGE = "Bereite einen Ablauf vor und spiele ihn Schritt fuer Schritt ab."; const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1"; let mode = "queens"; @@ -26,20 +25,56 @@ let currentSolverHighlight = null; const boardDiv = document.getElementById("board"); const statusMessage = document.getElementById("statusMessage"); const queenList = document.getElementById("queenList"); +const pieceCountBadge = document.getElementById("pieceCountBadge"); +const navDashboardLink = document.getElementById("navDashboardLink"); +const langSwitch = document.getElementById("langSwitch"); +const accountLabel = document.getElementById("accountLabel"); +const introKicker = document.getElementById("introKicker"); +const introTitle = document.getElementById("introTitle"); +const introText = document.getElementById("introText"); +const infoButton = document.getElementById("infoButton"); +const gameModeEyebrow = document.getElementById("gameModeEyebrow"); +const gameTitle = document.getElementById("gameTitle"); +const boardModeText = document.getElementById("boardModeText"); +const boardSizeInfoPrefix = document.getElementById("boardSizeInfoPrefix"); +const pieceListTitle = document.getElementById("pieceListTitle"); +const infoModalTitle = document.getElementById("infoModalTitle"); +const infoModeRuleText = document.getElementById("infoModeRuleText"); +const infoModalProblemHeading = document.getElementById("infoModalProblemHeading"); +const infoModalProblemText = document.getElementById("infoModalProblemText"); +const stepsLabel = document.getElementById("stepsLabel"); +const timeLabel = document.getElementById("timeLabel"); +const statusLabel = document.getElementById("statusLabel"); const stepCounterValue = document.getElementById("stepCounterValue"); const timerValue = document.getElementById("timerValue"); const gameProgressState = document.getElementById("gameProgressState"); +const solvedCelebration = document.getElementById("solvedCelebration"); +const solvedCelebrationTitle = document.getElementById("solvedCelebrationTitle"); +const solvedCelebrationText = document.getElementById("solvedCelebrationText"); const btnQueens = document.getElementById("btnQueens"); const btnRooks = document.getElementById("btnRooks"); const btnPrepareSolve = document.getElementById("btnPrepareSolve"); const btnInstantSolve = document.getElementById("btnInstantSolve"); const btnNextStep = document.getElementById("btnNextStep"); const btnPlayPause = document.getElementById("btnPlayPause"); +const btnReset = document.getElementById("btnReset"); +const btnSave = document.getElementById("btnSave"); +const btnLoad = document.getElementById("btnLoad"); const solverMessage = document.getElementById("solverMessage"); const solverStepInfo = document.getElementById("solverStepInfo"); +const solverKicker = document.getElementById("solverKicker"); +const solverTitle = document.getElementById("solverTitle"); +const animationSolverTitle = document.getElementById("animationSolverTitle"); +const animationSolverText = document.getElementById("animationSolverText"); +const instantSolverTitle = document.getElementById("instantSolverTitle"); +const instantSolverText = document.getElementById("instantSolverText"); const solverSpeedInput = document.getElementById("solverSpeedInput"); const solverSpeedValue = document.getElementById("solverSpeedValue"); +const solverSpeedLabel = document.getElementById("solverSpeedLabel"); const settingsSidebar = document.getElementById("settingsSidebar"); +const settingsTrigger = document.getElementById("settingsTrigger"); +const settingsTriggerArrow = document.getElementById("settingsTriggerArrow"); +const settingsTriggerLabel = document.getElementById("settingsTriggerLabel"); const sidebarBackdrop = document.getElementById("sidebarBackdrop"); const saveSidebar = document.getElementById("saveSidebar"); const saveBackdrop = document.getElementById("saveBackdrop"); @@ -50,18 +85,369 @@ const boardSizeValue = document.getElementById("boardSizeValue"); const boardSizeValueMirror = document.getElementById("boardSizeValueMirror"); const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue"); const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror"); +const settingsSidebarTitle = document.getElementById("settingsSidebarTitle"); +const settingsSidebarClose = document.getElementById("settingsSidebarClose"); +const settingsSidebarText = document.getElementById("settingsSidebarText"); +const boardSizeInputLabel = document.getElementById("boardSizeInputLabel"); +const applyBoardSizeButton = document.getElementById("applyBoardSizeButton"); +const settingsSidebarHint = document.getElementById("settingsSidebarHint"); const savePointsList = document.getElementById("savePointsList"); const saveNameInput = document.getElementById("saveNameInput"); const saveNoteInput = document.getElementById("saveNoteInput"); const saveFavoriteInput = document.getElementById("saveFavoriteInput"); +const saveSidebarTitle = document.getElementById("saveSidebarTitle"); +const saveSidebarClose = document.getElementById("saveSidebarClose"); +const saveSidebarText = document.getElementById("saveSidebarText"); +const saveNameLabel = document.getElementById("saveNameLabel"); +const saveNoteLabel = document.getElementById("saveNoteLabel"); +const saveFavoriteLabel = document.getElementById("saveFavoriteLabel"); +const saveNowButton = document.getElementById("saveNowButton"); const savePointModeFilter = document.getElementById("savePointModeFilter"); const savePointSizeFilter = document.getElementById("savePointSizeFilter"); const savePointSortFilter = document.getElementById("savePointSortFilter"); const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly"); +const savePointsSidebarTitle = document.getElementById("savePointsSidebarTitle"); +const savePointsSidebarClose = document.getElementById("savePointsSidebarClose"); +const savePointsSidebarText = document.getElementById("savePointsSidebarText"); +const savePointModeFilterLabel = document.getElementById("savePointModeFilterLabel"); +const savePointSizeFilterLabel = document.getElementById("savePointSizeFilterLabel"); +const savePointSortFilterLabel = document.getElementById("savePointSortFilterLabel"); +const savePointFavoritesOnlyLabel = document.getElementById("savePointFavoritesOnlyLabel"); +const savePointsEmptyState = document.getElementById("savePointsEmptyState"); +const historyKicker = document.getElementById("historyKicker"); +const historyTitle = document.getElementById("historyTitle"); +const historySubtitle = document.getElementById("historySubtitle"); +const btnRefreshHistory = document.getElementById("btnRefreshHistory"); +const historyHeadSave = document.getElementById("historyHeadSave"); +const historyHeadMode = document.getElementById("historyHeadMode"); +const historyHeadBoard = document.getElementById("historyHeadBoard"); +const historyHeadSteps = document.getElementById("historyHeadSteps"); +const historyHeadTime = document.getElementById("historyHeadTime"); +const historyHeadStatus = document.getElementById("historyHeadStatus"); +const historyHeadSaved = document.getElementById("historyHeadSaved"); const timeHistoryTableBody = document.getElementById("timeHistoryTableBody"); const infoModal = document.getElementById("infoModal"); const infoModalBackdrop = document.getElementById("infoModalBackdrop"); +const SUPPORTED_LANGUAGES = ["de", "en"]; +let currentLanguage = "de"; + +const MODE_COPY = { + de: { + queens: { + singular: "Dame", + plural: "Damen", + eyebrow: "Damen-Modus", + gameTitle: "Damen-Spielbrett", + boardText: "Platziere Damen so, dass keine zwei dieselbe Spalte oder Diagonale teilen.", + infoTitle: "Spielregeln und Acht-Damen-Problem", + infoRuleText: "Im Damen-Modus dürfen sich Damen weder in derselben Spalte noch diagonal angreifen. Mit einem Klick setzt oder entfernst du eine Figur.", + infoProblemHeading: "Was ist das Acht-Damen-Problem?", + infoProblemText: "Das klassische Acht-Damen-Problem fragt nach einer Anordnung von acht Damen auf einem 8x8-Brett, bei der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel für kombinatorische Suche und Backtracking." + }, + rooks: { + singular: "Turm", + plural: "Türme", + eyebrow: "Türme-Modus", + gameTitle: "Türme-Spielbrett", + boardText: "Platziere Türme so, dass keine zwei dieselbe Spalte teilen.", + infoTitle: "Spielregeln und Acht-Türme-Problem", + infoRuleText: "Im Türme-Modus duerfen sich Türme nicht in derselben Spalte stehen. Diagonalen spielen hier keine Rolle. Mit einem Klick setzt oder entfernst du eine Figur.", + infoProblemHeading: "Was ist das Acht-Türme-Problem?", + infoProblemText: "Beim Acht-Türme-Problem suchst du eine Anordnung von acht Türmen auf einem 8x8-Brett, bei der sich keine zwei Türme angreifen. Es ist eine einfachere Variante, um systematische Suche und Konfliktprüfung sichtbar zu machen." + } + }, + en: { + queens: { + singular: "Queen", + plural: "Queens", + eyebrow: "Queens Mode", + gameTitle: "Queens Board", + boardText: "Place queens so that no two share the same column or diagonal.", + infoTitle: "Rules and the Eight Queens Puzzle", + infoRuleText: "In queens mode, queens may not attack each other in the same column or on diagonals. Click once to place or remove a piece.", + infoProblemHeading: "What is the Eight Queens puzzle?", + infoProblemText: "The classic Eight Queens puzzle asks for eight queens on an 8x8 board so that no two queens attack each other. It is a well-known example of combinatorial search and backtracking." + }, + rooks: { + singular: "Rook", + plural: "Rooks", + eyebrow: "Rooks Mode", + gameTitle: "Rooks Board", + boardText: "Place rooks so that no two share the same column.", + infoTitle: "Rules and the Eight Rooks Puzzle", + infoRuleText: "In rooks mode, rooks may not stand in the same column. Diagonals do not matter here. Click once to place or remove a piece.", + infoProblemHeading: "What is the Eight Rooks puzzle?", + infoProblemText: "In the Eight Rooks puzzle, you arrange eight rooks on an 8x8 board so that no two rooks attack each other. It is a simpler variant that helps visualize systematic search and conflict checking." + } + } +}; + +const UI_TRANSLATIONS = { + de: { + navDashboard: "Dashboard", + accountLabel: "Account", + introKicker: "Puzzle-Modus", + introTitle: "Wähl deinen Modus und löse das Brett auf deine Art.", + introText: "Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die Brett-Einstellungen jederzeit ueber den linken Seiten-Tab.", + infoButton: "Regeln & Hilfe", + stepsLabel: "Schritte", + timeLabel: "Zeit", + statusLabel: "Status", + progressRunning: "Läuft", + progressSolved: "Gelöst", + boardSizePrefix: ({ modeLabel }) => `Aktuelle Brettgrösse im ${modeLabel}-Modus:`, + solverKicker: "Solver", + solverTitle: "Lösungswerkzeuge", + animationSolverTitle: "Animations-Solver", + animationSolverText: "Bereite den Lösungsweg vor, gehe ihn manuell Schritt für Schritt durch oder spiele die Animation automatisch ab.", + instantSolverTitle: "Sofort-Lösung", + instantSolverText: "Lade direkt eine gültige Lösung, ohne Zwischenschritte oder Animation.", + defaultSolverMessage: "Bereite einen Ablauf vor und spiele ihn Schritt für Schritt ab.", + solverReady: ({ modeLabel }) => `Solver für ${modeLabel} bereit. ${t("defaultSolverMessage")}`, + solverSpeedLabel: "Animationsgeschwindigkeit", + btnPrepareSolve: "Animation vorbereiten", + btnPrepareSolveLoading: "Animation wird vorbereitet...", + btnNextStep: "Nächster Schritt", + btnPlayPause: "Animation abspielen", + btnPlayReplay: "Animation erneut abspielen", + btnPlayPauseRunning: "Animation pausieren", + btnInstantSolve: "Sofort lösen", + btnReset: "Brett zurücksetzen", + settingsTrigger: "Brett-Einstellungen", + settingsSidebarTitle: "Brett-Einstellungen", + settingsSidebarClose: "Schliessen", + settingsSidebarText: "Passe die Brettgroesse zwischen 4 und 13 an.", + boardSizeInputLabel: "Grösse", + applyBoardSizeButton: "Grösse anwenden", + settingsSidebarHint: "Beim ändern wird das Brett zur Sicherheit zurückgesetzt. Auf dem Handy ist 8x8 eingeplant, grössere Bretter können abgeschnitten wirken.", + btnSave: "Speichern", + btnLoad: "Save-Points", + saveSidebarTitle: "Speicherpunkt", + saveSidebarClose: "Schliessen", + saveSidebarText: "Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.", + saveNameLabel: "Name", + saveNamePlaceholder: "z. B. Fast gelöste Dame 8x8", + saveNoteLabel: "Notiz", + saveNotePlaceholder: "z. B. Gute Startposition fuer Türmetest", + saveFavoriteLabel: "Als Favorit markieren", + saveNowButton: "Jetzt speichern", + savePointsSidebarTitle: "Save-Points", + savePointsSidebarClose: "Schliessen", + savePointsSidebarText: "Wähle einen Speicherstand aus der Liste.", + savePointModeFilterLabel: "Modus", + savePointSizeFilterLabel: "Brettgrösse", + savePointSortFilterLabel: "Sortierung", + savePointFavoritesOnlyLabel: "Nur Favoriten anzeigen", + savePointsEmptyState: "Noch keine Speicherstände gefunden.", + filterAll: "Alle", + filterNewest: "Neueste zuerst", + filterOldest: "älteste zuerst", + filterUpdated: "Zuletzt bearbeitet", + filterFavorites: "Favoriten zuerst", + historyKicker: "Save-Points", + historyTitle: "Gespeicherte Zeiten und Fortschritte", + historySubtitle: "Die Tabelle zeigt gespeicherte Save-Points mit Zeit, Schritten und Status.", + btnRefreshHistory: "Aktualisieren", + historyHeadSave: "Save-Point", + historyHeadMode: "Modus", + historyHeadBoard: "Brett", + historyHeadSteps: "Schritte", + historyHeadTime: "Zeit", + historyHeadStatus: "Status", + historyHeadSaved: "Gespeichert", + pieceListTitle: ({ modeLabel }) => `Positionen der ${modeLabel}`, + historyEmpty: "Noch keine gespeicherten Zeiten vorhanden.", + placedWord: "gesetzt", + noSavePointsFiltered: "Keine Speicherstände fuer diese Filter gefunden.", + previewEmpty: "Noch keine Figuren gesetzt", + createdLabel: "Erstellt", + updatedLabel: "Aktualisiert", + previewLabel: "Vorschau", + noteLabel: "Notiz", + noNote: "Keine Notiz", + stepsMeta: "Schritte", + timeMeta: "Zeit", + solvedMeta: "Gelöst", + openMeta: "Offen", + loadButton: "Laden", + favoriteAddButton: "Als Favorit", + favoriteRemoveButton: "Favorit entfernen", + deleteButton: "Löschen", + celebrationTitle: "Richtig geloöst!", + celebrationText: ({ modeLabel, duration }) => `Stark gemacht. Deine ${modeLabel.toLowerCase()}-Lösung ist konfliktfrei in ${duration}.`, + settingsApplied: ({ size }) => `Brettgrösse auf ${size} x ${size} gesetzt.`, + modeActivated: ({ piece }) => `${piece}-Modus aktiviert.`, + solverDiscardedManual: "Solver-Ablauf verworfen, weil das Brett manuell geändert wurde.", + solverDiscardedStatus: "Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", + solvedPerfect: ({ duration }) => `Perfekt gelöst in ${duration}.`, + animationReset: "Animations-Ablauf zurückgesetzt. Starte die Animation erneut oder gehe weiter mit Nächster Schritt.", + animationPreparing: ({ modeLabel, size }) => `Berechne den Animations-Ablauf für ${modeLabel.toLowerCase()} auf ${size} x ${size}...`, + animationPreparingStatus: ({ size }) => `Animations-Solver berechnet die Schritte fuer ${size} x ${size}.`, + noSolution: "Keine Lösung gefunden.", + noSolutionStatus: "Keine Lösung fuer dieses Brett gefunden.", + animationReady: ({ steps, queensMode }) => queensMode + ? `Animations-Ablauf bereit mit ${steps} Schritten. Nutze Nächster Schritt oder Animation abspielen, um das Backtracking zu verfolgen.` + : `Animations-Ablauf bereit mit ${steps} Schritten. Nutze Nächster Schritt oder Animation abspielen.`, + animationReadyStatus: "Animations-Solver bereit. Nutze Nächster Schritt oder Animation abspielen.", + animationLoadError: "Der Solver-Ablauf konnte nicht geladen werden.", + animationFinishedStatus: ({ size }) => `Loesung für ${size} x ${size} Schritt fuer Schritt aufgebaut.`, + animationAlreadyFinished: "Der Animations-Ablauf ist bereits abgeschlossen.", + animationPaused: "Animations-Solver pausiert.", + animationRunning: "Animations-Solver läuft.", + instantLoading: ({ modeLabel, size }) => `Lade die Sofort-Lösung für ${modeLabel.toLowerCase()} auf ${size} x ${size}...`, + instantLoadingStatus: "Sofort-Lösung wird geladen.", + instantLoaded: "Sofort-Lösung geladen. Starte den Animations-Solver, wenn du den Lösungsweg sehen möchtest.", + instantLoadedStatus: ({ size }) => `Sofort-Loesung für ${size} x ${size} geladen.`, + instantLoadError: "Die Sofort-Lösung konnte nicht geladen werden.", + saveError: "Der Speicherstand konnte nicht gespeichert werden.", + saveSuccess: ({ name }) => `Speicherpunkt "${name}" gespeichert.`, + loadError: "Dieser Speicherstand konnte nicht geladen werden.", + loadSuccess: ({ name }) => `Speicherpunkt "${name}" geladen.`, + favoriteError: "Favorit konnte nicht aktualisiert werden.", + favoriteSuccess: "Favoritenstatus aktualisiert.", + deleteConfirm: "Diesen Speicherpunkt wirklich löschen?", + deleteError: "Speicherpunkt konnte nicht gelöscht werden.", + deleteSuccess: "Speicherpunkt geloescht.", + pieceCount: ({ count }) => `${count} gesetzt`, + pieceEmpty: ({ modeLabel }) => `Noch keine ${modeLabel.toLowerCase()} gesetzt.`, + saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} gesetzt`, + speedValue: ({ speed }) => `${speed} ms`, + languageSwitchAria: "Sprache wechseln" + }, + en: { + navDashboard: "Dashboard", + accountLabel: "Account", + introKicker: "Puzzle Mode", + introTitle: "Choose your mode and solve the board your way.", + introText: "Switch between queens and rooks, use animation or instant solve, and open board settings anytime from the left side tab.", + infoButton: "Rules & Help", + stepsLabel: "Steps", + timeLabel: "Time", + statusLabel: "Status", + progressRunning: "Running", + progressSolved: "Solved", + boardSizePrefix: ({ modeLabel }) => `Current board size in ${modeLabel} mode:`, + solverKicker: "Solver", + solverTitle: "Solving Tools", + animationSolverTitle: "Animation Solver", + animationSolverText: "Prepare the solving path, step through it manually, or play the animation automatically.", + instantSolverTitle: "Instant Solve", + instantSolverText: "Load a valid solution directly, without intermediate steps or animation.", + defaultSolverMessage: "Prepare a solving trace and play it back step by step.", + solverReady: ({ modeLabel }) => `Solver for ${modeLabel} is ready. ${t("defaultSolverMessage")}`, + solverSpeedLabel: "Animation speed", + btnPrepareSolve: "Prepare animation", + btnPrepareSolveLoading: "Preparing animation...", + btnNextStep: "Next step", + btnPlayPause: "Play animation", + btnPlayReplay: "Replay animation", + btnPlayPauseRunning: "Pause animation", + btnInstantSolve: "Solve instantly", + btnReset: "Reset board", + settingsTrigger: "Board settings", + settingsSidebarTitle: "Board Settings", + settingsSidebarClose: "Close", + settingsSidebarText: "Adjust the board size between 4 and 13.", + boardSizeInputLabel: "Size", + applyBoardSizeButton: "Apply size", + settingsSidebarHint: "Changing the size resets the board for safety. On phones, 8x8 is the intended maximum for a reliable fit and larger boards may appear cropped.", + btnSave: "Save", + btnLoad: "Save points", + saveSidebarTitle: "Save Point", + saveSidebarClose: "Close", + saveSidebarText: "Give the current run a clear name and optionally a short note.", + saveNameLabel: "Name", + saveNamePlaceholder: "e.g. Nearly solved queen 8x8", + saveNoteLabel: "Note", + saveNotePlaceholder: "e.g. Strong starting position for rook test", + saveFavoriteLabel: "Mark as favorite", + saveNowButton: "Save now", + savePointsSidebarTitle: "Save Points", + savePointsSidebarClose: "Close", + savePointsSidebarText: "Choose a saved state from the list.", + savePointModeFilterLabel: "Mode", + savePointSizeFilterLabel: "Board size", + savePointSortFilterLabel: "Sorting", + savePointFavoritesOnlyLabel: "Show favorites only", + savePointsEmptyState: "No saved states yet.", + filterAll: "All", + filterNewest: "Newest first", + filterOldest: "Oldest first", + filterUpdated: "Recently updated", + filterFavorites: "Favorites first", + historyKicker: "Save Points", + historyTitle: "Saved Times and Progress", + historySubtitle: "The table shows saved runs with time, steps, and status.", + btnRefreshHistory: "Refresh", + historyHeadSave: "Save Point", + historyHeadMode: "Mode", + historyHeadBoard: "Board", + historyHeadSteps: "Steps", + historyHeadTime: "Time", + historyHeadStatus: "Status", + historyHeadSaved: "Saved", + pieceListTitle: ({ modeLabel }) => `${modeLabel} positions`, + historyEmpty: "No saved times yet.", + placedWord: "placed", + noSavePointsFiltered: "No saved states found for these filters.", + previewEmpty: "No pieces placed yet", + createdLabel: "Created", + updatedLabel: "Updated", + previewLabel: "Preview", + noteLabel: "Note", + noNote: "No note", + stepsMeta: "Steps", + timeMeta: "Time", + solvedMeta: "Solved", + openMeta: "Open", + loadButton: "Load", + favoriteAddButton: "Favorite", + favoriteRemoveButton: "Remove favorite", + deleteButton: "Delete", + celebrationTitle: "Correctly solved!", + celebrationText: ({ modeLabel, duration }) => `Well done. Your ${modeLabel.toLowerCase()} solution is conflict-free in ${duration}.`, + settingsApplied: ({ size }) => `Board size set to ${size} x ${size}.`, + modeActivated: ({ piece }) => `${piece} mode activated.`, + solverDiscardedManual: "Solver trace discarded because the board was changed manually.", + solverDiscardedStatus: "Solver trace discarded. Prepare it again if needed.", + solvedPerfect: ({ duration }) => `Perfectly solved in ${duration}.`, + animationReset: "Animation trace reset. Start the animation again or continue with Next step.", + animationPreparing: ({ modeLabel, size }) => `Preparing the animation trace for ${modeLabel.toLowerCase()} on ${size} x ${size}...`, + animationPreparingStatus: ({ size }) => `Animation solver is preparing the steps for ${size} x ${size}.`, + noSolution: "No solution found.", + noSolutionStatus: "No solution found for this board.", + animationReady: ({ steps, queensMode }) => queensMode + ? `Animation trace ready with ${steps} steps. Use Next step or Play animation to follow the backtracking process.` + : `Animation trace ready with ${steps} steps. Use Next step or Play animation.`, + animationReadyStatus: "Animation solver ready. Use Next step or Play animation.", + animationLoadError: "The solver trace could not be loaded.", + animationFinishedStatus: ({ size }) => `Solution for ${size} x ${size} was built step by step.`, + animationAlreadyFinished: "The animation trace is already complete.", + animationPaused: "Animation solver paused.", + animationRunning: "Animation solver is running.", + instantLoading: ({ modeLabel, size }) => `Loading the instant solution for ${modeLabel.toLowerCase()} on ${size} x ${size}...`, + instantLoadingStatus: "Instant solution is loading.", + instantLoaded: "Instant solution loaded. Start the animation solver if you want to see the solving path.", + instantLoadedStatus: ({ size }) => `Instant solution for ${size} x ${size} loaded.`, + instantLoadError: "The instant solution could not be loaded.", + saveError: "The save point could not be saved.", + saveSuccess: ({ name }) => `Save point "${name}" saved.`, + loadError: "This save point could not be loaded.", + loadSuccess: ({ name }) => `Save point "${name}" loaded.`, + favoriteError: "Favorite status could not be updated.", + favoriteSuccess: "Favorite status updated.", + deleteConfirm: "Really delete this save point?", + deleteError: "Save point could not be deleted.", + deleteSuccess: "Save point deleted.", + pieceCount: ({ count }) => `${count} placed`, + pieceEmpty: ({ modeLabel }) => `No ${modeLabel.toLowerCase()} placed yet.`, + saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} placed`, + speedValue: ({ speed }) => `${speed} ms`, + languageSwitchAria: "Switch language" + } +}; + function createEmptyBoard(size) { return Array(size).fill(EMPTY_CELL); } @@ -74,6 +460,44 @@ function getColumnLabel(index) { return String.fromCharCode(65 + index); } +function normalizeLanguage(language) { + const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); + return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : "de"; +} + +function getStoredLanguage() { + try { + return window.localStorage.getItem("preferredLanguage"); + } catch (error) { + return null; + } +} + +function storeLanguage(language) { + try { + window.localStorage.setItem("preferredLanguage", language); + } catch (error) { + return; + } +} + +function detectBrowserLanguage() { + return normalizeLanguage(navigator.language || navigator.userLanguage || "de"); +} + +function t(key, params = {}) { + const value = UI_TRANSLATIONS[currentLanguage][key]; + return typeof value === "function" ? value(params) : value; +} + +function getDefaultSolverMessage() { + return t("defaultSolverMessage"); +} + +function getModeCopy(selectedMode = mode, language = currentLanguage) { + return MODE_COPY[language][selectedMode === "rooks" ? "rooks" : "queens"]; +} + function sanitizeBoard(candidateBoard, size) { if (!Array.isArray(candidateBoard) || candidateBoard.length !== size) { return createEmptyBoard(size); @@ -130,8 +554,9 @@ function persistCurrentSession() { function updateProgressDisplay() { stepCounterValue.textContent = String(stepCount); timerValue.textContent = formatDuration(Math.floor(getElapsedMilliseconds() / 1000)); - gameProgressState.textContent = isSolved ? "Geloest" : "Laeuft"; + gameProgressState.textContent = isSolved ? t("progressSolved") : t("progressRunning"); gameProgressState.style.color = isSolved ? "#15803d" : "#1d4ed8"; + updateSolvedCelebration(); } function stopGameTimer() { @@ -187,6 +612,121 @@ function ensureFreshRunAfterSolvedBoard() { function updateModeButtons() { btnQueens.classList.toggle("active", mode === "queens"); btnRooks.classList.toggle("active", mode === "rooks"); + btnQueens.classList.toggle("secondary", mode !== "queens"); + btnRooks.classList.toggle("secondary", mode !== "rooks"); +} + +function updateModeContent() { + const copy = getModeCopy(mode); + + gameModeEyebrow.textContent = copy.eyebrow; + gameTitle.textContent = copy.gameTitle; + boardModeText.textContent = copy.boardText; + pieceListTitle.textContent = t("pieceListTitle", { modeLabel: copy.plural }); + infoModalTitle.textContent = copy.infoTitle; + infoModeRuleText.textContent = copy.infoRuleText; + infoModalProblemHeading.textContent = copy.infoProblemHeading; + infoModalProblemText.textContent = copy.infoProblemText; + boardSizeInfoPrefix.textContent = t("boardSizePrefix", { modeLabel: copy.plural }); +} + +function updateSolvedCelebration() { + if (!isSolved) { + solvedCelebration.hidden = true; + return; + } + + solvedCelebrationTitle.textContent = t("celebrationTitle"); + solvedCelebrationText.textContent = t("celebrationText", { + modeLabel: getModeLabel(mode), + duration: formatDuration(Math.floor(getElapsedMilliseconds() / 1000)) + }); + solvedCelebration.hidden = false; +} + +function applyLanguageContent() { + document.documentElement.lang = currentLanguage; + navDashboardLink.textContent = t("navDashboard"); + accountLabel.textContent = t("accountLabel"); + introKicker.textContent = t("introKicker"); + introTitle.textContent = t("introTitle"); + introText.textContent = t("introText"); + infoButton.textContent = t("infoButton"); + infoButton.setAttribute("aria-label", t("infoButton")); + stepsLabel.textContent = t("stepsLabel"); + timeLabel.textContent = t("timeLabel"); + statusLabel.textContent = t("statusLabel"); + solverKicker.textContent = t("solverKicker"); + solverTitle.textContent = t("solverTitle"); + animationSolverTitle.textContent = t("animationSolverTitle"); + animationSolverText.textContent = t("animationSolverText"); + instantSolverTitle.textContent = t("instantSolverTitle"); + instantSolverText.textContent = t("instantSolverText"); + solverSpeedLabel.textContent = t("solverSpeedLabel"); + btnNextStep.textContent = t("btnNextStep"); + btnInstantSolve.textContent = t("btnInstantSolve"); + btnReset.textContent = t("btnReset"); + settingsSidebarTitle.textContent = t("settingsSidebarTitle"); + settingsSidebarClose.textContent = t("settingsSidebarClose"); + settingsSidebarText.textContent = t("settingsSidebarText"); + boardSizeInputLabel.textContent = t("boardSizeInputLabel"); + applyBoardSizeButton.textContent = t("applyBoardSizeButton"); + settingsSidebarHint.textContent = t("settingsSidebarHint"); + btnSave.textContent = t("btnSave"); + btnLoad.textContent = t("btnLoad"); + saveSidebarTitle.textContent = t("saveSidebarTitle"); + saveSidebarClose.textContent = t("saveSidebarClose"); + saveSidebarText.textContent = t("saveSidebarText"); + saveNameLabel.textContent = t("saveNameLabel"); + saveNameInput.placeholder = t("saveNamePlaceholder"); + saveNoteLabel.textContent = t("saveNoteLabel"); + saveNoteInput.placeholder = t("saveNotePlaceholder"); + saveFavoriteLabel.textContent = t("saveFavoriteLabel"); + saveNowButton.textContent = t("saveNowButton"); + savePointsSidebarTitle.textContent = t("savePointsSidebarTitle"); + savePointsSidebarClose.textContent = t("savePointsSidebarClose"); + savePointsSidebarText.textContent = t("savePointsSidebarText"); + savePointModeFilterLabel.textContent = t("savePointModeFilterLabel"); + savePointSizeFilterLabel.textContent = t("savePointSizeFilterLabel"); + savePointSortFilterLabel.textContent = t("savePointSortFilterLabel"); + savePointFavoritesOnlyLabel.textContent = t("savePointFavoritesOnlyLabel"); + savePointsEmptyState.textContent = t("savePointsEmptyState"); + historyKicker.textContent = t("historyKicker"); + historyTitle.textContent = t("historyTitle"); + historySubtitle.textContent = t("historySubtitle"); + btnRefreshHistory.textContent = t("btnRefreshHistory"); + historyHeadSave.textContent = t("historyHeadSave"); + historyHeadMode.textContent = t("historyHeadMode"); + historyHeadBoard.textContent = t("historyHeadBoard"); + historyHeadSteps.textContent = t("historyHeadSteps"); + historyHeadTime.textContent = t("historyHeadTime"); + historyHeadStatus.textContent = t("historyHeadStatus"); + historyHeadSaved.textContent = t("historyHeadSaved"); + + savePointModeFilter.options[0].textContent = t("filterAll"); + savePointModeFilter.options[1].textContent = getModeCopy("queens").plural; + savePointModeFilter.options[2].textContent = getModeCopy("rooks").plural; + savePointSizeFilter.options[0].textContent = t("filterAll"); + savePointSortFilter.options[0].textContent = t("filterNewest"); + savePointSortFilter.options[1].textContent = t("filterOldest"); + savePointSortFilter.options[2].textContent = t("filterUpdated"); + savePointSortFilter.options[3].textContent = t("filterFavorites"); + + langSwitch.textContent = currentLanguage.toUpperCase(); + langSwitch.setAttribute("aria-label", t("languageSwitchAria")); + langSwitch.title = currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln"; + + updateModeContent(); + updateQueenList(); + updateProgressDisplay(); + updateSolverSpeed(solverSpeedMs); + updateSolverControls(); +} + +function updateSettingsTrigger(isOpen = settingsSidebar.classList.contains("open")) { + settingsTrigger.setAttribute("aria-expanded", String(isOpen)); + settingsTriggerArrow.textContent = isOpen ? "←" : "→"; + settingsTriggerLabel.textContent = t("settingsTrigger"); } function updateBoardSizeDisplays(size) { @@ -202,14 +742,14 @@ function setStatus(message = "", color = "") { statusMessage.style.color = color; } -function setSolverMessage(message = DEFAULT_SOLVER_MESSAGE) { +function setSolverMessage(message = getDefaultSolverMessage()) { solverMessage.textContent = message; } function updateSolverSpeed(rawValue) { solverSpeedMs = Number(rawValue); solverSpeedInput.value = String(solverSpeedMs); - solverSpeedValue.textContent = `${solverSpeedMs} ms`; + solverSpeedValue.textContent = t("speedValue", { speed: solverSpeedMs }); if (isSolverPlaying()) { stopSolverPlayback(false); @@ -218,10 +758,19 @@ function updateSolverSpeed(rawValue) { } function updateBoardLayout() { - const cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize))); + let cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize))); + let labelSize = cellSize; + + if (window.innerWidth <= 760) { + const labelRatio = 0.72; + const availableWidth = Math.max(250, window.innerWidth - 26); + cellSize = Math.max(24, Math.floor(availableWidth / (boardSize + labelRatio))); + labelSize = Math.max(18, Math.floor(cellSize * labelRatio)); + } boardDiv.style.setProperty("--board-size", boardSize); boardDiv.style.setProperty("--cell-size", `${cellSize}px`); + boardDiv.style.setProperty("--label-size", `${labelSize}px`); } function isSolverPlaying() { @@ -255,20 +804,20 @@ function updateSolverControls() { btnNextStep.disabled = solverLoading || hasCompletedSolverTrace(); btnPlayPause.disabled = solverLoading; - btnPrepareSolve.textContent = solverLoading ? "Preparing..." : "Solve Step-by-Step"; + btnPrepareSolve.textContent = solverLoading ? t("btnPrepareSolveLoading") : t("btnPrepareSolve"); if (isSolverPlaying()) { - btnPlayPause.textContent = "Pause"; + btnPlayPause.textContent = t("btnPlayPauseRunning"); } else if (hasCompletedSolverTrace()) { - btnPlayPause.textContent = "Replay Animation"; + btnPlayPause.textContent = t("btnPlayReplay"); } else { - btnPlayPause.textContent = "Play Animation"; + btnPlayPause.textContent = t("btnPlayPause"); } solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`; } -function discardSolverTrace(message = DEFAULT_SOLVER_MESSAGE) { +function discardSolverTrace(message = getDefaultSolverMessage()) { solverRequestId += 1; solverLoading = false; stopSolverPlayback(false); @@ -286,7 +835,7 @@ function rewindSolverTrace() { solverStepIndex = -1; currentSolverHighlight = null; board = createEmptyBoard(boardSize); - setSolverMessage("Ablauf zurueckgesetzt. Starte die Animation erneut oder gehe weiter mit Next Step."); + setSolverMessage(t("animationReset")); drawBoard(); updateSolverControls(); } @@ -298,10 +847,11 @@ function setMode(newMode) { mode = newMode; updateModeButtons(); + updateModeContent(); resetBoard({ - statusMessageText: `${getPieceLabel(mode)}-Modus aktiviert.`, + statusMessageText: t("modeActivated", { piece: getPieceLabel(mode) }), statusColor: "#2563eb", - solverMessageText: `${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}` + solverMessageText: t("solverReady", { modeLabel: getModeLabel(mode) }) }); } @@ -357,23 +907,6 @@ function finishSolvedRun() { persistCurrentSession(); } -function checkSolution() { - const allPlaced = board.every((col) => col !== EMPTY_CELL); - - if (!allPlaced) { - setStatus("Noch nicht alle Figuren platziert.", "#d97706"); - return; - } - - if (getConflicts().length > 0) { - setStatus("Es gibt Konflikte auf dem Brett.", "red"); - return; - } - - finishSolvedRun(); - setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green"); -} - function drawBoard() { boardDiv.innerHTML = ""; boardDiv.className = "board-grid"; @@ -433,8 +966,8 @@ function placeQueen(row, col) { ensureFreshRunAfterSolvedBoard(); if (hasPreparedSolverTrace() || isSolverPlaying()) { - discardSolverTrace("Solver-Ablauf verworfen, weil das Brett manuell geaendert wurde."); - setStatus("Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", "#d97706"); + discardSolverTrace(t("solverDiscardedManual")); + setStatus(t("solverDiscardedStatus"), "#d97706"); } else { setStatus(); } @@ -447,7 +980,7 @@ function placeQueen(row, col) { if (isBoardSolved()) { finishSolvedRun(); - setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green"); + setStatus(t("solvedPerfect", { duration: formatDuration(Math.floor(getElapsedMilliseconds() / 1000)) }), "green"); } else { persistCurrentSession(); } @@ -461,12 +994,12 @@ function previewBoardSize(size) { function applyBoardSize() { boardSize = pendingBoardSize; - discardSolverTrace(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`); + discardSolverTrace(t("solverReady", { modeLabel: getModeLabel(mode) })); board = createEmptyBoard(boardSize); currentSolverHighlight = null; resetProgress(); updateBoardSizeDisplays(boardSize); - setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb"); + setStatus(t("settingsApplied", { size: boardSize }), "#2563eb"); drawBoard(); toggleSidebar(false); } @@ -482,6 +1015,7 @@ function toggleSidebar(forceOpen) { settingsSidebar.classList.toggle("open", shouldOpen); sidebarBackdrop.classList.toggle("visible", shouldOpen); settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); + updateSettingsTrigger(shouldOpen); } function toggleSavePanel(forceOpen) { @@ -519,7 +1053,7 @@ function toggleInfoModal(forceOpen) { } function getPieceLabel(selectedMode) { - return selectedMode === "queens" ? "Dame" : "Turm"; + return getModeCopy(selectedMode).singular; } function getPlacedPieceCount() { @@ -527,7 +1061,11 @@ function getPlacedPieceCount() { } function resetSaveForm() { - saveNameInput.value = `${getPieceLabel(mode)} ${boardSize}x${boardSize} - ${getPlacedPieceCount()} gesetzt`; + saveNameInput.value = t("saveDefaultName", { + piece: getPieceLabel(mode), + size: boardSize, + count: getPlacedPieceCount() + }); saveNoteInput.value = ""; saveFavoriteInput.checked = false; } @@ -552,7 +1090,7 @@ function getBoardPreview(savedBoard, limit = 4) { } } - return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt"; + return preview.length > 0 ? preview.join(", ") : t("previewEmpty"); } function getSavePointFilters() { @@ -585,14 +1123,14 @@ function createSavePointActionButton(label, className, onClick) { } function getModeLabel(selectedMode) { - return selectedMode === "rooks" ? "Tuerme" : "Damen"; + return getModeCopy(selectedMode).plural; } function renderSavePoints(savePoints) { savePointsList.innerHTML = ""; if (savePoints.length === 0) { - savePointsList.innerHTML = ''; + savePointsList.innerHTML = ``; return; } @@ -609,28 +1147,28 @@ function renderSavePoints(savePoints) { item.className = "save-point-card"; title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`; createdAt.className = "save-point-meta"; - createdAt.textContent = `Erstellt: ${savePoint.created_at}`; + createdAt.textContent = `${t("createdLabel")}: ${savePoint.created_at}`; updatedAt.className = "save-point-meta"; - updatedAt.textContent = `Aktualisiert: ${savePoint.updated_at}`; + updatedAt.textContent = `${t("updatedLabel")}: ${savePoint.updated_at}`; boardMeta.className = "save-point-meta"; - boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`; + boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} ${t("placedWord")}`; preview.className = "save-point-preview"; - preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`; + preview.textContent = `${t("previewLabel")}: ${getBoardPreview(savePoint.board)}`; note.className = "save-point-note"; - note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz"; + note.textContent = savePoint.save_note ? `${t("noteLabel")}: ${savePoint.save_note}` : `${t("noteLabel")}: ${t("noNote")}`; actions.className = "save-point-actions"; const progressMeta = document.createElement("p"); progressMeta.className = "save-point-meta"; - progressMeta.textContent = `Schritte: ${savePoint.step_count || 0} | Zeit: ${formatDuration(savePoint.elapsed_seconds)} | Status: ${savePoint.is_solved ? "Geloest" : "Offen"}`; + progressMeta.textContent = `${t("stepsMeta")}: ${savePoint.step_count || 0} | ${t("timeMeta")}: ${formatDuration(savePoint.elapsed_seconds)} | ${t("statusLabel")}: ${savePoint.is_solved ? t("solvedMeta") : t("openMeta")}`; - actions.appendChild(createSavePointActionButton("Laden", "btn", () => loadSavePoint(savePoint.id))); + actions.appendChild(createSavePointActionButton(t("loadButton"), "btn", () => loadSavePoint(savePoint.id))); actions.appendChild(createSavePointActionButton( - savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit", + savePoint.is_favorite ? t("favoriteRemoveButton") : t("favoriteAddButton"), "btn secondary", () => toggleSavePointFavorite(savePoint.id) )); - actions.appendChild(createSavePointActionButton("Loeschen", "btn ghost", () => deleteSavePoint(savePoint.id))); + actions.appendChild(createSavePointActionButton(t("deleteButton"), "btn ghost", () => deleteSavePoint(savePoint.id))); item.appendChild(title); item.appendChild(createdAt); @@ -648,7 +1186,7 @@ function renderTimeHistory(savePoints) { timeHistoryTableBody.innerHTML = ""; if (savePoints.length === 0) { - timeHistoryTableBody.innerHTML = 'Noch keine gespeicherten Zeiten vorhanden.'; + timeHistoryTableBody.innerHTML = `${t("historyEmpty")}`; return; } @@ -660,7 +1198,7 @@ function renderTimeHistory(savePoints) { `${savePoint.board_size} x ${savePoint.board_size}`, String(savePoint.step_count || 0), formatDuration(savePoint.elapsed_seconds), - savePoint.is_solved ? "Geloest" : "Offen", + savePoint.is_solved ? t("solvedMeta") : t("openMeta"), savePoint.updated_at ]; @@ -698,7 +1236,7 @@ async function openSavePointsPanel() { } function applyLoadedGame(data) { - discardSolverTrace(`${getPieceLabel(data.mode || "queens")}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`); + discardSolverTrace(t("solverReady", { modeLabel: getModeLabel(data.mode || "queens") })); const restoredBoardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE; @@ -714,6 +1252,7 @@ function applyLoadedGame(data) { }); updateModeButtons(); + updateModeContent(); updateBoardSizeDisplays(boardSize); drawBoard(); updateProgressDisplay(); @@ -742,7 +1281,7 @@ function applySolverStep(step) { currentSolverHighlight = null; } - setSolverMessage(step.message || DEFAULT_SOLVER_MESSAGE); + setSolverMessage(step.message || getDefaultSolverMessage()); drawBoard(); persistCurrentSession(); updateSolverControls(); @@ -761,7 +1300,7 @@ function advanceSolverStep() { if (hasCompletedSolverTrace()) { finishSolvedRun(); stopSolverPlayback(false); - setStatus(`Loesung fuer ${boardSize} x ${boardSize} Schritt fuer Schritt aufgebaut.`, "green"); + setStatus(t("animationFinishedStatus", { size: boardSize }), "green"); updateSolverControls(); } @@ -814,8 +1353,8 @@ async function prepareSolutionTrace() { solverSteps = []; solverStepIndex = -1; currentSolverHighlight = null; - setSolverMessage(`Berechne ${getPieceLabel(mode)}-Ablauf fuer ${boardSize} x ${boardSize}...`); - setStatus(`Solver berechnet die Schritte fuer ${boardSize} x ${boardSize}.`, "#2563eb"); + setSolverMessage(t("animationPreparing", { modeLabel: getModeLabel(mode), size: boardSize })); + setStatus(t("animationPreparingStatus", { size: boardSize }), "#2563eb"); updateSolverControls(); try { @@ -837,19 +1376,15 @@ async function prepareSolutionTrace() { currentSolverHighlight = null; if (!trace.solved || solverSteps.length === 0) { - setSolverMessage("Keine Loesung gefunden."); - setStatus("Keine Loesung fuer dieses Brett gefunden.", "red"); + setSolverMessage(t("noSolution")); + setStatus(t("noSolutionStatus"), "red"); drawBoard(); persistCurrentSession(); return false; } - setSolverMessage( - mode === "queens" - ? `Ablauf bereit mit ${solverSteps.length} Schritten. Nutze Next Step oder Play Animation, um das Backtracking zu verfolgen.` - : `Ablauf bereit mit ${solverSteps.length} Schritten. Nutze Next Step oder starte die Animation.` - ); - setStatus("Solver-Ablauf bereit. Nutze Next Step oder Play Animation.", "#2563eb"); + setSolverMessage(t("animationReady", { steps: solverSteps.length, queensMode: mode === "queens" })); + setStatus(t("animationReadyStatus"), "#2563eb"); drawBoard(); return true; } catch (error) { @@ -857,8 +1392,8 @@ async function prepareSolutionTrace() { return false; } - setSolverMessage("Der Solver-Ablauf konnte nicht geladen werden."); - setStatus("Der Solver-Ablauf konnte nicht geladen werden.", "red"); + setSolverMessage(t("animationLoadError")); + setStatus(t("animationLoadError"), "red"); return false; } finally { if (requestId === solverRequestId) { @@ -886,7 +1421,7 @@ async function nextSolverStep() { } if (!advanceSolverStep()) { - setStatus("Der Solver-Ablauf ist bereits abgeschlossen.", "#2563eb"); + setStatus(t("animationAlreadyFinished"), "#2563eb"); } } @@ -897,7 +1432,7 @@ async function toggleSolverPlayback() { if (isSolverPlaying()) { stopSolverPlayback(); - setStatus("Solver-Animation pausiert.", "#2563eb"); + setStatus(t("animationPaused"), "#2563eb"); return; } @@ -913,7 +1448,7 @@ async function toggleSolverPlayback() { rewindSolverTrace(); } - setStatus("Solver-Animation laeuft.", "#2563eb"); + setStatus(t("animationRunning"), "#2563eb"); startSolverPlayback(true); } @@ -934,8 +1469,8 @@ async function instantSolve() { solverSteps = []; solverStepIndex = -1; currentSolverHighlight = null; - setSolverMessage(`Lade sofortige ${getPieceLabel(mode)}-Loesung fuer ${boardSize} x ${boardSize}...`); - setStatus(`Sofortige Loesung wird geladen.`, "#2563eb"); + setSolverMessage(t("instantLoading", { modeLabel: getModeLabel(mode), size: boardSize })); + setStatus(t("instantLoadingStatus"), "#2563eb"); updateSolverControls(); try { @@ -954,8 +1489,8 @@ async function instantSolve() { board = mode === "queens" && Array.isArray(solution[0]) ? solution[0].slice() : solution.slice(); currentSolverHighlight = null; finishSolvedRun(); - setSolverMessage("Sofortige Loesung geladen. Starte Solve Step-by-Step, wenn du den Ablauf sehen moechtest."); - setStatus(`Loesung fuer ${boardSize} x ${boardSize} sofort geladen.`, "green"); + setSolverMessage(t("instantLoaded")); + setStatus(t("instantLoadedStatus", { size: boardSize }), "green"); drawBoard(); persistCurrentSession(); } catch (error) { @@ -963,8 +1498,8 @@ async function instantSolve() { return; } - setSolverMessage("Die sofortige Loesung konnte nicht geladen werden."); - setStatus("Die sofortige Loesung konnte nicht geladen werden.", "red"); + setSolverMessage(t("instantLoadError")); + setStatus(t("instantLoadError"), "red"); } finally { if (requestId === solverRequestId) { solverLoading = false; @@ -977,7 +1512,7 @@ function resetBoard(options = {}) { const { statusMessageText = "", statusColor = "", - solverMessageText = `${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}` + solverMessageText = t("solverReady", { modeLabel: getModeLabel(mode) }) } = options; discardSolverTrace(solverMessageText); @@ -996,7 +1531,6 @@ function resetBoard(options = {}) { function updateQueenList() { queenList.innerHTML = ""; - let count = 1; for (let row = 0; row < boardSize; row++) { @@ -1008,10 +1542,20 @@ function updateQueenList() { const position = `${getColumnLabel(col)}${boardSize - row}`; const li = document.createElement("li"); - li.textContent = `${mode === "queens" ? "Dame" : "Turm"} ${count}: ${position}`; + li.textContent = `${getPieceLabel(mode)} ${count}: ${position}`; queenList.appendChild(li); count += 1; } + + const placedPieces = count - 1; + pieceCountBadge.textContent = t("pieceCount", { count: placedPieces }); + + if (placedPieces === 0) { + const emptyItem = document.createElement("li"); + emptyItem.className = "empty"; + emptyItem.textContent = t("pieceEmpty", { modeLabel: getModeLabel(mode) }); + queenList.appendChild(emptyItem); + } } async function saveGame() { @@ -1034,7 +1578,7 @@ async function saveGame() { }); if (!response.ok) { - setStatus("Der Speicherstand konnte nicht gespeichert werden.", "red"); + setStatus(t("saveError"), "red"); return; } @@ -1043,7 +1587,7 @@ async function saveGame() { toggleSavePanel(false); await refreshSavePoints(); await refreshTimeHistory(); - setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green"); + setStatus(t("saveSuccess", { name: data.save_point.save_name }), "green"); } async function loadGame() { @@ -1056,14 +1600,14 @@ async function loadSavePoint(savePointId) { const response = await fetch(`/load/${savePointId}`); if (!response.ok) { - setStatus("Dieser Speicherstand konnte nicht geladen werden.", "red"); + setStatus(t("loadError"), "red"); return; } const data = await response.json(); applyLoadedGame(data); - setStatus(`Speicherpunkt "${data.save_name}" geladen.`, "green"); + setStatus(t("loadSuccess", { name: data.save_name }), "green"); toggleSavePointsPanel(false); } @@ -1073,17 +1617,17 @@ async function toggleSavePointFavorite(savePointId) { }); if (!response.ok) { - setStatus("Favorit konnte nicht aktualisiert werden.", "red"); + setStatus(t("favoriteError"), "red"); return; } await refreshSavePoints(); await refreshTimeHistory(); - setStatus("Favoritenstatus aktualisiert.", "#2563eb"); + setStatus(t("favoriteSuccess"), "#2563eb"); } async function deleteSavePoint(savePointId) { - const confirmed = window.confirm("Diesen Speicherpunkt wirklich loeschen?"); + const confirmed = window.confirm(t("deleteConfirm")); if (!confirmed) { return; @@ -1094,13 +1638,13 @@ async function deleteSavePoint(savePointId) { }); if (!response.ok) { - setStatus("Speicherpunkt konnte nicht geloescht werden.", "red"); + setStatus(t("deleteError"), "red"); return; } await refreshSavePoints(); await refreshTimeHistory(); - setStatus("Speicherpunkt geloescht.", "#2563eb"); + setStatus(t("deleteSuccess"), "#2563eb"); } function restoreSessionFromStorage() { @@ -1122,6 +1666,7 @@ function restoreSessionFromStorage() { currentSolverHighlight = null; updateModeButtons(); + updateModeContent(); updateBoardSizeDisplays(boardSize); drawBoard(); resetProgress({ @@ -1137,12 +1682,13 @@ function restoreSessionFromStorage() { } async function initializeDashboard() { + currentLanguage = normalizeLanguage(getStoredLanguage() || detectBrowserLanguage()); pendingBoardSize = boardSize; updateModeButtons(); updateBoardSizeDisplays(boardSize); - updateSolverSpeed(solverSpeedMs); - setSolverMessage(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`); - updateSolverControls(); + applyLanguageContent(); + updateSettingsTrigger(false); + setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) })); if (!restoreSessionFromStorage()) { resetProgress(); @@ -1153,10 +1699,28 @@ async function initializeDashboard() { await refreshTimeHistory(); } +if (langSwitch) { + langSwitch.addEventListener("click", async () => { + currentLanguage = currentLanguage === "de" ? "en" : "de"; + storeLanguage(currentLanguage); + applyLanguageContent(); + resetSaveForm(); + await refreshTimeHistory(); + + if (savePointsSidebar.classList.contains("open")) { + await refreshSavePoints(); + } + }); +} + window.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeAllPanels(); } }); +window.addEventListener("resize", () => { + drawBoard(); +}); + initializeDashboard(); diff --git a/app/static/js/index.js b/app/static/js/index.js new file mode 100644 index 0000000..be170c2 --- /dev/null +++ b/app/static/js/index.js @@ -0,0 +1,238 @@ +const HERO_BOARD_SIZE = 8; +const heroBoard = document.getElementById("heroBoard"); +const langSwitch = document.getElementById("langSwitch"); +const supportedLanguages = ["de", "en"]; +const initialLanguage = window.LANDING_INITIAL_LANGUAGE || "de"; + +const translations = { + de: { + "meta.title": "8-Damen- und 8-Türme Solver", + "nav.brand": "8-Damen- und 8-Türme Solver", + "nav.problem": "Problem", + "nav.features": "Features", + "nav.journey": "Ablauf", + "nav.login": "Login", + "nav.signup": "Create Account", + "hero.eyebrow": "Interaktives Lernen mit Strategie, Logik und Backtracking", + "hero.title": "Entdecke das Acht-Damen- und Acht-Türme-Problem.", + "hero.text": "Diese App macht aus einem klassischen Informatik- und Mathematikproblem ein interaktives Erlebnis: Setze Damen oder Türme, verfolge den Solver Schritt fuer Schritt, miss deine Zeit und speichere clevere Zwischenstände.", + "hero.primaryCta": "Jetzt starten", + "hero.secondaryCta": "Bereits Account? Login", + "hero.fact1Title": "Skalierbar", + "hero.fact1Text": "Brettgrösse flexibel anpassen", + "hero.fact2Title": "Schritt für Schritt", + "hero.fact2Text": "Solver mit Animation", + "hero.fact3Title": "Save-Points", + "hero.fact3Text": "Zeit und Fortschritt behalten", + "hero.solverLabel": "Solver", + "hero.solverValue": "Backtracking live", + "hero.challengeLabel": "Challenge", + "hero.challengeValue": "Zeit und Schritte messen", + "problem.eyebrow": "Warum dieses Problem spannend ist", + "problem.title": "Die Acht-Damen- und Acht-Türme-Probleme sind mehr als Schachrätsel.", + "problem.text": "Beim Acht-Damen-Problem suchst du eine Anordnung von acht Damen auf einem Schachbrett, sodass sich keine zwei Damen schlagen. Beim Acht-Tuerme-Problem gilt dieselbe Idee für Türme, nur mit einfacheren Konfliktregeln. Beide Aufgaben zeigen, wie Mustererkennung, systematische Suche und Backtracking funktionieren.

Gerade beim Damenproblem wird schnell sichtbar, warum Strategie wichtig ist: Es gibt unzählige moegliche Platzierungen, aber nur sehr wenige wirklich gültige Loesungen. Genau das macht diese App spannend, weil man nicht nur das Ergebnis sieht, sondern auch den Weg dorthin.", + "features.eyebrow": "Was die Applikation bietet", + "features.title": "Von der Idee bis zur gelösten Stellung.", + "features.card1Title": "Interaktives Brett", + "features.card1Text": "Setze Damen oder Türme direkt auf dem Brett und prüfe, ob deine Anordnung konfliktfrei ist.", + "features.card2Title": "Solver mit Animation", + "features.card2Text": "Beobachte den Loesungsweg Schritt fuer Schritt und verstehe, wie Backtracking arbeitet.", + "features.card3Title": "Zeit und Schritte", + "features.card3Text": "Miss deinen Fortschritt während des Spiels und vergleiche unterschiedliche Versuche.", + "features.card4Title": "Save-Points", + "features.card4Text": "Speichere Zwischenstände mit Notizen, Zeiten und Schrittzahl fuer späteres Weiterarbeiten.", + "features.card5Title": "Skalierbare Bretter", + "features.card5Text": "Passe die Brettgrösse an und erkunde die Probleme nicht nur auf dem klassischen 8x8-Brett.", + "journey.eyebrow": "So funktioniert dein Einstieg", + "journey.title": "In wenigen Schritten vom ersten Besuch zur eigenen Lösung.", + "journey.step1Title": "Account erstellen", + "journey.step1Text": "Registriere dich und erhalte Zugang zu Dashboard, Save-Points und deinem Fortschritt.", + "journey.step2Title": "Spielmodus wählen", + "journey.step2Text": "Arbeite mit Damen oder Türmen und passe die Brettgrösse deinem Ziel an.", + "journey.step3Title": "Loesen oder analysieren", + "journey.step3Text": "Teste eigene Ideen oder lass dir die Lösung visuell vom Solver lösen lassen, dabei wird 1:1 gezeigt wie der Solver vorgeht.", + "cta.eyebrow": "Bereit für den ersten Versuch?", + "cta.title": "Mach aus einem bekannten Problem ein sichtbares Lernerlebnis.", + "cta.text": "Starte mit einem Account, speichere deine Zwischenstände und finde heraus, wie schnell du eine konfliktfreie Lösung erreichst.", + "cta.primaryCta": "Create Account", + "cta.secondaryCta": "Login" + }, + en: { + "meta.title": "8 Queens and 8 Rooks Solver", + "nav.brand": "8 Queens and 8 Rooks Solver", + "nav.problem": "Problem", + "nav.features": "Features", + "nav.journey": "Journey", + "nav.login": "Login", + "nav.signup": "Create Account", + "hero.eyebrow": "Interactive learning with strategy, logic, and backtracking", + "hero.title": "Discover the Eight Queens and Eight Rooks puzzle.", + "hero.text": "This app turns a classic computer science and mathematics problem into an interactive experience: place queens or rooks, follow the solver step by step, track your time, and save clever checkpoints.", + "hero.primaryCta": "Get Started", + "hero.secondaryCta": "Already have an account? Login", + "hero.fact1Title": "Scalable", + "hero.fact1Text": "Adjust the board size freely", + "hero.fact2Title": "Step by step", + "hero.fact2Text": "Animated solver walkthrough", + "hero.fact3Title": "Save points", + "hero.fact3Text": "Keep your time and progress", + "hero.solverLabel": "Solver", + "hero.solverValue": "Backtracking in motion", + "hero.challengeLabel": "Challenge", + "hero.challengeValue": "Measure time and steps", + "problem.eyebrow": "Why this puzzle matters", + "problem.title": "The Eight Queens and Eight Rooks problems are more than chess puzzles.", + "problem.text": "In the Eight Queens problem, you search for a way to place eight queens on a chessboard so that no two queens can attack each other. The Eight Rooks problem follows the same idea for rooks, but with simpler conflict rules. Both tasks show how pattern recognition, systematic search, and backtracking work.

The queens puzzle quickly makes it obvious why strategy matters: there are countless possible placements, but only a few truly valid solutions. That is what makes this app interesting, because you do not just see the result, you also see the path to it.", + "features.eyebrow": "What the application offers", + "features.title": "From the first idea to a solved position.", + "features.card1Title": "Interactive board", + "features.card1Text": "Place queens or rooks directly on the board and check whether your setup is conflict-free.", + "features.card2Title": "Animated solver", + "features.card2Text": "Watch the solving path step by step and understand how backtracking works.", + "features.card3Title": "Time and steps", + "features.card3Text": "Track your progress during play and compare different attempts.", + "features.card4Title": "Save points", + "features.card4Text": "Store checkpoints with notes, time, and step count so you can continue later.", + "features.card5Title": "Scalable boards", + "features.card5Text": "Adjust the board size and explore the puzzle beyond the classic 8x8 board.", + "journey.eyebrow": "How your first session works", + "journey.title": "Reach your own solution in just a few steps.", + "journey.step1Title": "Create an account", + "journey.step1Text": "Sign up and unlock the dashboard, save points, and your tracked progress.", + "journey.step2Title": "Choose a game mode", + "journey.step2Text": "Work with queens or rooks and tune the board size to your goal.", + "journey.step3Title": "Solve or analyze", + "journey.step3Text": "Test your own ideas or let the solver show the solution visually, with a 1:1 view of each solving step.", + "cta.eyebrow": "Ready for your first attempt?", + "cta.title": "Turn a famous puzzle into a visible learning experience.", + "cta.text": "Start with an account, save your checkpoints, and find out how quickly you can reach a conflict-free solution.", + "cta.primaryCta": "Create Account", + "cta.secondaryCta": "Login" + } +}; + +const queenPatterns = [ + [0, 4, 7, 5, 2, 6, 1, 3], + [1, 3, 5, 7, 2, 0, 6, 4], + [2, 5, 7, 0, 3, 6, 4, 1] +]; + +let currentPatternIndex = 0; + +function normalizeLanguage(language) { + const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); + return supportedLanguages.includes(shortLanguage) ? shortLanguage : "de"; +} + +function getStoredLanguage() { + try { + return window.localStorage.getItem("preferredLanguage"); + } catch (error) { + return null; + } +} + +function storeLanguage(language) { + try { + window.localStorage.setItem("preferredLanguage", language); + } catch (error) { + return; + } +} + +function detectBrowserLanguage() { + const browserLanguage = navigator.language || navigator.userLanguage || initialLanguage; + return normalizeLanguage(browserLanguage); +} + +function setLanguage(language) { + const resolvedLanguage = normalizeLanguage(language); + const dictionary = translations[resolvedLanguage]; + const i18nElements = document.querySelectorAll("[data-i18n]"); + + document.documentElement.lang = resolvedLanguage; + document.body.dataset.language = resolvedLanguage; + + i18nElements.forEach((element) => { + const key = element.dataset.i18n; + const translatedText = dictionary[key]; + + if (!translatedText) { + return; + } + + if (element.tagName === "TITLE" || translatedText.includes(" { + const row = Number(cell.dataset.row); + const col = Number(cell.dataset.col); + const hasQueen = pattern[row] === col; + + cell.classList.toggle("queen", hasQueen); + cell.classList.toggle("highlight", hasQueen); + }); +} + +function rotatePattern() { + currentPatternIndex = (currentPatternIndex + 1) % queenPatterns.length; + applyPattern(queenPatterns[currentPatternIndex]); +} + +if (langSwitch) { + langSwitch.addEventListener("click", () => { + const nextLanguage = document.documentElement.lang === "de" ? "en" : "de"; + setLanguage(nextLanguage); + }); +} + +createHeroBoard(); +applyPattern(queenPatterns[currentPatternIndex]); +setLanguage(getPreferredLanguage()); + +if (heroBoard) { + window.setInterval(rotatePattern, 2200); +} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 93129e1..c1c37c9 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -10,18 +10,39 @@ +