Initial commit: Sudoku game with accounts, history, leaderboard, themes and i18n

Node/Express + SQLite backend, vanilla JS frontend, deployed via Docker Compose.
This commit is contained in:
2026-08-27 21:24:38 +02:00
commit 00e8dcb7d2
16 changed files with 2405 additions and 0 deletions

227
app/index.html Normal file
View File

@@ -0,0 +1,227 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Sudoku</title>
<link rel="stylesheet" href="styles.css">
<script>
(function(){
try{
var theme = localStorage.getItem('sudoku-theme');
if(theme) document.documentElement.dataset.theme = theme;
var lang = localStorage.getItem('sudoku-lang');
if(lang) document.documentElement.lang = lang;
}catch(e){}
})();
</script>
</head>
<body>
<div class="app">
<h1>🧩 Sudoku</h1>
<div class="settings-bar">
<div class="lang-switch" id="langSwitch">
<button class="lang-btn" data-lang="fr">FR</button>
<button class="lang-btn" data-lang="en">EN</button>
</div>
<div class="theme-switch" id="themeSwitch">
<button class="theme-swatch" data-theme="blue"></button>
<button class="theme-swatch" data-theme="green"></button>
<button class="theme-swatch" data-theme="amber"></button>
<button class="theme-swatch" data-theme="mono"></button>
</div>
</div>
<div class="authbar" id="authbar"></div>
<div class="anon-banner" id="anonBanner" hidden>
<div class="txt" data-i18n-html="banner.html"></div>
<div class="btns">
<button id="anonBannerRegister" class="primary" data-i18n="auth.register"></button>
<button id="anonBannerLogin" data-i18n="auth.login"></button>
</div>
<button class="close-x" id="anonBannerClose" title="Fermer">✕</button>
</div>
<div class="topbar">
<div class="stat"><span data-i18n="topbar.difficulty"></span>
<select id="difficulty">
<option value="easy" data-i18n="difficulty.easy"></option>
<option value="medium" selected data-i18n="difficulty.medium"></option>
<option value="hard" data-i18n="difficulty.hard"></option>
<option value="expert" data-i18n="difficulty.expert"></option>
</select>
</div>
<div class="stat"><span data-i18n="topbar.time"></span> <span class="value" id="timer">00:00</span></div>
<div class="stat"><span data-i18n="topbar.mistakes"></span> <span class="value" id="mistakes">0</span></div>
</div>
<div class="board-wrap" id="boardWrap">
<div class="board" id="board"></div>
<div class="board-overlay" id="startOverlay">
<p data-i18n="board.ready"></p>
<button class="big-btn" id="startBtn" data-i18n="board.start"></button>
</div>
<div class="board-overlay" id="pauseOverlay" hidden>
<p data-i18n="board.paused"></p>
<button class="big-btn" id="resumeBtn" data-i18n="board.resume"></button>
</div>
</div>
<div class="keypad" id="keypad"></div>
<div class="actions" id="gameActions" hidden>
<button id="pauseBtn" data-i18n="actions.pause"></button>
<button id="stopBtn" data-i18n="actions.stop"></button>
<button id="undoBtn" data-i18n="actions.undo"></button>
<button id="eraseBtn" data-i18n="actions.erase"></button>
<button id="notesBtn" data-i18n="actions.notes"></button>
<button id="hintBtn" data-i18n="actions.hint"></button>
</div>
<div class="actions">
<button id="rulesBtn" data-i18n="actions.rules"></button>
<button id="historyBtn" data-i18n="actions.history"></button>
<button id="leaderboardBtn" data-i18n="actions.leaderboard"></button>
</div>
<div class="msg" id="msg"></div>
<footer data-i18n="footer.text"></footer>
</div>
<!-- Win modal -->
<div class="modal-overlay" id="winModal">
<div class="modal-card win">
<button class="modal-close">✕</button>
<h2 data-i18n="win.title"></h2>
<p class="sub" id="winSubtitle"></p>
<div class="win-stats">
<div class="stat"><span class="v" id="winTime"></span><span class="l" data-i18n="win.time"></span></div>
<div class="stat"><span class="v" id="winMistakes"></span><span class="l" data-i18n="win.mistakesLabel"></span></div>
</div>
<p class="sub" id="winPoints"></p>
<div id="winCta" hidden style="margin-bottom:14px;">
<p class="sub" style="margin-top:0;" data-i18n="win.ctaText"></p>
<button class="full-btn" id="winRegisterBtn" data-i18n="win.ctaButton"></button>
</div>
<button class="full-btn" id="winCloseBtn" data-i18n="win.replay"></button>
</div>
</div>
<!-- Auth modal -->
<div class="modal-overlay" id="authModal">
<div class="modal-card">
<button class="modal-close">✕</button>
<div class="tabs">
<button id="authTabLogin" class="active" data-i18n="auth.loginTab"></button>
<button id="authTabRegister" data-i18n="auth.registerTab"></button>
</div>
<p class="error-text" id="authError"></p>
<form id="loginForm" class="stack">
<div class="field">
<label for="loginIdentifier" data-i18n="auth.identifierLabel"></label>
<input type="text" id="loginIdentifier" required autocomplete="username">
</div>
<div class="field">
<label for="loginPassword" data-i18n="auth.passwordLabel"></label>
<input type="password" id="loginPassword" required autocomplete="current-password">
</div>
<button type="submit" class="full-btn" data-i18n="auth.submitLogin"></button>
</form>
<form id="registerForm" class="stack" hidden>
<ul class="benefits-list">
<li data-i18n="auth.benefit1"></li>
<li data-i18n="auth.benefit2"></li>
<li data-i18n="auth.benefit3"></li>
</ul>
<div class="field">
<label for="registerPseudo" data-i18n="auth.pseudoLabel"></label>
<input type="text" id="registerPseudo" required minlength="3" maxlength="20" autocomplete="username">
</div>
<div class="field">
<label for="registerEmail" data-i18n="auth.emailLabel"></label>
<input type="email" id="registerEmail" required autocomplete="email">
</div>
<div class="field">
<label for="registerPassword" data-i18n="auth.passwordLabel"></label>
<input type="password" id="registerPassword" required minlength="6" autocomplete="new-password">
</div>
<button type="submit" class="full-btn" data-i18n="auth.submitRegister"></button>
</form>
</div>
</div>
<!-- History modal -->
<div class="modal-overlay" id="historyModal">
<div class="modal-card" style="max-width:480px;">
<button class="modal-close">✕</button>
<h2 data-i18n="history.title"></h2>
<div id="historyBody"></div>
</div>
</div>
<!-- Leaderboard modal -->
<div class="modal-overlay" id="leaderboardModal">
<div class="modal-card" style="max-width:480px;">
<button class="modal-close">✕</button>
<h2 data-i18n="leaderboard.title"></h2>
<div class="tabs">
<button id="lbTabTimes" class="active" data-i18n="leaderboard.tabTimes"></button>
<button id="lbTabPoints" data-i18n="leaderboard.tabPoints"></button>
</div>
<div class="diff-tabs" id="lbDiffTabs">
<button data-diff="easy" data-i18n="difficulty.easy"></button>
<button data-diff="medium" class="active" data-i18n="difficulty.medium"></button>
<button data-diff="hard" data-i18n="difficulty.hard"></button>
<button data-diff="expert" data-i18n="difficulty.expert"></button>
</div>
<div id="leaderboardBody"></div>
</div>
</div>
<!-- Rules / beginner's guide modal -->
<div class="modal-overlay" id="rulesModal">
<div class="modal-card rules" style="max-width:480px;">
<button class="modal-close">✕</button>
<h2 data-i18n="rules.title"></h2>
<h3 data-i18n="rules.objectiveTitle"></h3>
<p data-i18n="rules.objectiveText"></p>
<h3 data-i18n="rules.goldenRulesTitle"></h3>
<ol>
<li data-i18n="rules.rule1"></li>
<li data-i18n="rules.rule2"></li>
<li data-i18n="rules.rule3"></li>
</ol>
<h3 data-i18n="rules.howToTitle"></h3>
<ul>
<li data-i18n="rules.how1"></li>
<li data-i18n="rules.how2"></li>
<li data-i18n="rules.how3"></li>
<li data-i18n="rules.how4"></li>
<li data-i18n="rules.how5"></li>
</ul>
<h3 data-i18n="rules.tipsTitle"></h3>
<ul>
<li data-i18n="rules.tip1"></li>
<li data-i18n="rules.tip2"></li>
<li data-i18n="rules.tip3"></li>
<li data-i18n="rules.tip4"></li>
<li data-i18n="rules.tip5"></li>
</ul>
</div>
</div>
<script src="i18n.js"></script>
<script src="game.js"></script>
<script src="main.js"></script>
</body>
</html>