diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index 634fb38..a1a6404 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -1,26 +1,224 @@ -/* ==================== Base ==================== */ +: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%); } +a { + color: inherit; + text-decoration: none; +} -/* ==================== Layout And Cards ==================== */ +button, +input, +select, +textarea { + font: inherit; +} + +.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; +} + +.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; +} + +.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; +} .container { - max-width: 900px; - margin: 30px auto; - padding: 10px; + width: min(1200px, calc(100vw - 32px)); + margin: 0 auto; + padding: 34px 0 56px; } .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 { @@ -32,245 +230,257 @@ 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); } - -/* ==================== Navigation ==================== */ - -.navbar { - display: flex; - justify-content: space-between; - align-items: center; - background: #1e293b; - color: white; - padding: 15px 30px; +.section-kicker { + margin: 0 0 12px; + color: var(--navy-soft); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 13px; + font-weight: 700; } -.navbar a { - color: white; - margin-left: 15px; - text-decoration: none; +.dashboard-intro { + text-align: center; } -.navbar a:hover { - text-decoration: underline; +.dashboard-intro h1 { + margin: 0; + line-height: 0.96; + letter-spacing: -0.03em; + font-size: clamp(2.4rem, 4.4vw, 4.8rem); + color: #182029; } -.nav-right { - display: flex; - align-items: center; - flex-wrap: wrap; +.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); } -.nav-logout-form { - margin: 0 0 0 15px; +.dashboard-intro-text { + max-width: 760px; + margin: 18px auto 0; + font-size: 18px; } -.nav-link-button { - padding: 0; - border: none; - background: none; - color: white; - font: inherit; - cursor: pointer; -} - -.nav-link-button:hover { - text-decoration: underline; -} - - -/* ==================== Shared Buttons ==================== */ - .btn { - padding: 10px 15px; - margin: 5px; + min-height: 50px; + padding: 12px 18px; border: none; - border-radius: 8px; - background: #3b82f6; - color: white; + 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 { - background: #2563eb; + 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 { - background: #94a3b8; + transform: none; + opacity: 0.55; + box-shadow: none; cursor: not-allowed; } .btn.secondary { - background: #64748b; -} - -.btn.secondary:hover { - background: #475569; -} - -.btn.danger { - background: #ef4444; -} - -.btn.danger:hover { - background: #dc2626; -} - -.btn.active { - background: #22c55e; + background: linear-gradient(135deg, var(--navy), var(--navy-soft)); + color: #f9fbfd; } +.btn.danger, .btn.ghost { - background: #e2e8f0; - color: #1e293b; + background: rgba(255, 255, 255, 0.68); + color: var(--navy); + border: 1px solid rgba(31, 49, 64, 0.16); } -.btn.ghost:hover { - background: #cbd5e1; +.btn.active, +.btn.accent { + background: linear-gradient(135deg, var(--gold), #f0c35b); + color: #241b0d; } - -/* ==================== User Card Details ==================== */ - -.inline-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; -} - -.inline-actions form { - margin: 0; -} - -.inline-actions .btn { - margin: 0; -} - - -/* ==================== Mode Switch And Main Actions ==================== */ - .mode-switch { - margin-bottom: 15px; -} - -.mode-switch .btn { - margin: 5px; -} - -.controls { - margin-top: 15px; display: flex; justify-content: center; flex-wrap: wrap; - gap: 10px; + gap: 16px; + margin-top: 28px; } -.solver-controls .btn, -.controls .btn { - margin: 0; +.mode-switch .btn { + min-width: 220px; + min-height: 68px; + padding: 18px 28px; + font-size: 1.15rem; } - -/* ==================== Game Overview ==================== */ - .game-card { text-align: center; } .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: 0 0 10px; - color: #475569; + 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; +} -/* ==================== Chessboard ==================== */ +.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 { overflow-x: auto; @@ -278,14 +488,15 @@ body { } .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%; @@ -295,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 { @@ -311,7 +523,7 @@ 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 { @@ -319,27 +531,24 @@ body { } .white { - background: #e2e8f0; + background: #f7e6ca; } .black { - background: #475569; - color: white; + background: #92613a; + color: #fff5ea; } .invalid { - background: #ef4444 !important; + background: #d65555 !important; } - -/* ==================== Solver And Animation Details ==================== */ - .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; } @@ -351,31 +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-groups { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr); + gap: 16px; + margin-top: 18px; +} + +.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: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; - margin-top: 14px; + margin-top: 16px; +} + +.instant-solver-actions { + margin-top: 16px; +} + +.instant-solver-actions .btn { + width: 100%; } .solver-speed { @@ -383,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"] { @@ -391,69 +630,91 @@ 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; } -.cell.solver-place { - box-shadow: inset 0 0 0 4px #22c55e; - animation: solverPulse 0.35s ease; +.controls { + margin-top: 18px; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 10px; } -.cell.solver-remove { - box-shadow: inset 0 0 0 4px #f59e0b; - animation: solverPulse 0.35s ease; +.controls-save-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + width: min(420px, 100%); } -.cell.solver-solution { - box-shadow: inset 0 0 0 4px #2563eb; - animation: solverPulse 0.35s ease; +.settings-sidebar-actions { + margin-top: 18px; + display: grid; + gap: 10px; } - -/* ==================== Figure Details ==================== */ - -#queenList { - list-style: none; - padding: 0; -} - -#queenList li { - padding: 5px; - background: #f1f5f9; - margin: 4px 0; - border-radius: 6px; -} - - -/* ==================== Save History And Tables ==================== */ - -.history-header { +.section-header, +.history-header, +.sidebar-header, +.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; - margin-bottom: 14px; +} + +.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: 0; + margin-top: 4px; } .history-subtitle { - margin: 6px 0 0; - color: #64748b; + margin: 8px 0 0; + font-size: 15px; } .history-table-wrapper { overflow-x: auto; + margin-top: 18px; } .history-table { @@ -465,37 +726,35 @@ body { .history-table th, .history-table td { padding: 12px 10px; - border-bottom: 1px solid #e2e8f0; + 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: #334155; + color: var(--navy); font-size: 14px; } .history-table td { - color: #475569; + color: var(--muted); font-size: 14px; } .history-empty { - color: #64748b; + color: var(--muted); text-align: center !important; } - -/* ==================== Sidebars And Save Points ==================== */ - .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; @@ -509,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 { @@ -559,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 { @@ -567,11 +828,6 @@ body { font-size: 18px; } -.sidebar-hint { - color: #64748b; - font-size: 14px; -} - .save-points-list { display: grid; gap: 12px; @@ -583,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 { @@ -598,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; } @@ -624,13 +867,6 @@ body { margin-top: 14px; } -.save-point-actions .btn { - margin: 0; -} - - -/* ==================== Backdrops And Modal ==================== */ - .sidebar-backdrop { position: fixed; inset: 0; @@ -665,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; @@ -686,35 +922,30 @@ 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; +} -/* ==================== Animations ==================== */ +.cell.solver-solution { + box-shadow: inset 0 0 0 4px #2563eb; + animation: solverPulse 0.35s ease; +} @keyframes solverPulse { 0% { @@ -730,40 +961,128 @@ body { } } - -/* ==================== Responsive ==================== */ - -@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/login.css b/app/static/css/login.css index 1431a37..d4a1008 100644 --- a/app/static/css/login.css +++ b/app/static/css/login.css @@ -355,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 = '