v0.9.5 web client interactivity improvement
This commit is contained in:
@@ -4,21 +4,21 @@ go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.30.3
|
||||
github.com/gofiber/fiber/v2 v2.52.13
|
||||
github.com/gofiber/fiber/v2 v2.52.14
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226
|
||||
github.com/mattn/go-sqlite3 v1.14.45
|
||||
github.com/mattn/go-sqlite3 v1.14.47
|
||||
golang.org/x/term v0.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/compress v1.19.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
@@ -26,8 +26,8 @@ require (
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.71.0 // indirect
|
||||
github.com/valyala/fasthttp v1.72.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -24,6 +26,8 @@ github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/
|
||||
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
|
||||
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
|
||||
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -32,6 +36,8 @@ github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 h1:c7wfyZGdy6RkM/b6mIazoYrAS+3qDL7d9M1CFm2e1VA=
|
||||
@@ -56,6 +62,8 @@ github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCK
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -72,6 +80,8 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M=
|
||||
github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
@@ -99,5 +109,7 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -15,6 +15,8 @@ let gameState = {
|
||||
authToken: null,
|
||||
userId: null,
|
||||
username: null,
|
||||
authBusy: false,
|
||||
newGameBusy: false,
|
||||
};
|
||||
|
||||
// Chess piece Unicode: all black pieces for better fill, white pawn due to inability to override emoji variant display
|
||||
@@ -23,6 +25,26 @@ const pieceMap = {
|
||||
'P': '♙', 'R': '♜', 'N': '♞', 'B': '♝', 'Q': '♛', 'K': '♚'
|
||||
};
|
||||
|
||||
// How long a success message stays visible in a modal before it auto-closes
|
||||
const MODAL_SUCCESS_DISPLAY_MS = 700;
|
||||
|
||||
// Shared helpers: show/clear a status line inside a modal. Distinct from
|
||||
// flashErrorMessage, which is not visible while a modal's backdrop is up.
|
||||
function setModalMessage(elementId, message, type = 'error') {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
el.textContent = message;
|
||||
el.classList.remove('error', 'success');
|
||||
el.classList.add('show', type);
|
||||
}
|
||||
|
||||
function clearModalMessage(elementId) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
el.textContent = '';
|
||||
el.classList.remove('show', 'error', 'success');
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const config = await getConfig();
|
||||
@@ -115,6 +137,20 @@ function handleAuthClick() {
|
||||
}
|
||||
}
|
||||
|
||||
// Disables/enables every interactive control in the auth modal at once, and tracks
|
||||
// whether a login/register request is in flight (or its success message is showing).
|
||||
// Guards re-entrancy from the Enter-key handler, which bypasses individual button
|
||||
// disabled state, and stops the user editing fields or switching tabs mid-request.
|
||||
function setAuthModalBusy(busy) {
|
||||
gameState.authBusy = busy;
|
||||
document.getElementById('login-submit-btn').disabled = busy;
|
||||
document.getElementById('register-submit-btn').disabled = busy;
|
||||
document.getElementById('auth-cancel-btn').disabled = busy;
|
||||
document.getElementById('auth-cancel-btn-2').disabled = busy;
|
||||
document.querySelectorAll('.auth-tab').forEach(t => t.disabled = busy);
|
||||
document.querySelectorAll('.auth-form input').forEach(i => i.disabled = busy);
|
||||
}
|
||||
|
||||
function showAuthModal() {
|
||||
document.getElementById('auth-modal-overlay').classList.add('show');
|
||||
document.getElementById('login-identifier').focus();
|
||||
@@ -127,12 +163,21 @@ function hideAuthModal() {
|
||||
document.getElementById('auth-modal-overlay').classList.remove('show');
|
||||
document.querySelectorAll('.auth-form input').forEach(input => input.value = '');
|
||||
document.removeEventListener('keydown', handleAuthModalKeydown);
|
||||
clearModalMessage('auth-modal-message');
|
||||
setAuthModalBusy(false);
|
||||
}
|
||||
|
||||
function handleAuthModalKeydown(e) {
|
||||
const modal = document.getElementById('auth-modal-overlay');
|
||||
if (!modal.classList.contains('show')) return;
|
||||
|
||||
// While a request is in flight, block just Enter (re-submit) and Escape
|
||||
// (close); everything else (Tab, copy shortcuts, etc.) passes through.
|
||||
if (gameState.authBusy) {
|
||||
if (e.key === 'Enter' || e.key === 'Escape') e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
hideAuthModal();
|
||||
@@ -153,6 +198,7 @@ function switchAuthTab(tab) {
|
||||
|
||||
document.getElementById('login-form').style.display = tab === 'login' ? 'block' : 'none';
|
||||
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
|
||||
clearModalMessage('auth-modal-message');
|
||||
}
|
||||
|
||||
// Shared helper: safely parse error response regardless of Content-Type
|
||||
@@ -165,93 +211,125 @@ async function parseErrorResponse(response) {
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (gameState.authBusy) return;
|
||||
|
||||
const identifier = document.getElementById('login-identifier').value.trim();
|
||||
const password = document.getElementById('login-password').value;
|
||||
|
||||
if (!identifier || !password) {
|
||||
flashErrorMessage('Fill all fields');
|
||||
setModalMessage('auth-modal-message', 'Fill all fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById('login-submit-btn');
|
||||
submitBtn.disabled = true;
|
||||
setAuthModalBusy(true);
|
||||
clearModalMessage('auth-modal-message');
|
||||
|
||||
let response;
|
||||
try {
|
||||
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, {
|
||||
response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await parseErrorResponse(response);
|
||||
flashErrorMessage(err.details || err.error || 'Login failed', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await response.json();
|
||||
gameState.authToken = auth.token;
|
||||
gameState.userId = auth.userId;
|
||||
gameState.username = auth.username;
|
||||
localStorage.setItem('authToken', auth.token);
|
||||
updateAuthIndicator(true);
|
||||
hideAuthModal();
|
||||
} catch (error) {
|
||||
flashErrorMessage('Connection failed');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
const errorInfo = handleApiError('login', error);
|
||||
setModalMessage('auth-modal-message', errorInfo.statusMessage, 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await parseErrorResponse(response);
|
||||
setModalMessage('auth-modal-message', err.details || err.error || 'Login failed', 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let auth;
|
||||
try {
|
||||
auth = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Login: response OK but JSON parse failed:', error);
|
||||
setModalMessage('auth-modal-message', 'Unexpected response from server', 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
gameState.authToken = auth.token;
|
||||
gameState.userId = auth.userId;
|
||||
gameState.username = auth.username;
|
||||
localStorage.setItem('authToken', auth.token);
|
||||
updateAuthIndicator(true);
|
||||
|
||||
setModalMessage('auth-modal-message', `Logged in as ${auth.username}`, 'success');
|
||||
setTimeout(hideAuthModal, MODAL_SUCCESS_DISPLAY_MS);
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
if (gameState.authBusy) return;
|
||||
|
||||
const username = document.getElementById('register-username').value.trim();
|
||||
const email = document.getElementById('register-email').value.trim();
|
||||
const password = document.getElementById('register-password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
flashErrorMessage('Username and password required');
|
||||
setModalMessage('auth-modal-message', 'Username and password required', 'error');
|
||||
return;
|
||||
}
|
||||
if (password.length < 8) {
|
||||
flashErrorMessage('Password min 8 chars');
|
||||
setModalMessage('auth-modal-message', 'Password min 8 chars', 'error');
|
||||
return;
|
||||
}
|
||||
if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
|
||||
flashErrorMessage('Password needs a letter and number');
|
||||
setModalMessage('auth-modal-message', 'Password needs a letter and number', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById('register-submit-btn');
|
||||
submitBtn.disabled = true;
|
||||
setAuthModalBusy(true);
|
||||
clearModalMessage('auth-modal-message');
|
||||
|
||||
const body = { username, password };
|
||||
if (email) body.email = email;
|
||||
|
||||
let response;
|
||||
try {
|
||||
const body = { username, password };
|
||||
if (email) body.email = email;
|
||||
|
||||
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/register`, {
|
||||
response = await fetch(`${gameState.apiUrl}/api/v1/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await parseErrorResponse(response);
|
||||
flashErrorMessage(err.details || err.error || 'Registration failed', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await response.json();
|
||||
gameState.authToken = auth.token;
|
||||
gameState.userId = auth.userId;
|
||||
gameState.username = auth.username;
|
||||
localStorage.setItem('authToken', auth.token);
|
||||
updateAuthIndicator(true);
|
||||
hideAuthModal();
|
||||
} catch (error) {
|
||||
flashErrorMessage('Connection failed');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
const errorInfo = handleApiError('register', error);
|
||||
setModalMessage('auth-modal-message', errorInfo.statusMessage, 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await parseErrorResponse(response);
|
||||
setModalMessage('auth-modal-message', err.details || err.error || 'Registration failed', 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let auth;
|
||||
try {
|
||||
auth = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Register: response OK but JSON parse failed:', error);
|
||||
setModalMessage('auth-modal-message', 'Unexpected response from server', 'error');
|
||||
setAuthModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
gameState.authToken = auth.token;
|
||||
gameState.userId = auth.userId;
|
||||
gameState.username = auth.username;
|
||||
localStorage.setItem('authToken', auth.token);
|
||||
updateAuthIndicator(true);
|
||||
|
||||
setModalMessage('auth-modal-message', `Account created, welcome ${auth.username}`, 'success');
|
||||
setTimeout(hideAuthModal, MODAL_SUCCESS_DISPLAY_MS);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -375,6 +453,19 @@ function updateTurnIndicator(state, turn) {
|
||||
indicator.setAttribute('data-status', tooltipText);
|
||||
}
|
||||
|
||||
// Disables/enables every interactive control in the new-game modal at once, and
|
||||
// tracks whether a create-game request is in flight (or its success message is
|
||||
// showing). Same rationale as setAuthModalBusy.
|
||||
function setNewGameModalBusy(busy) {
|
||||
gameState.newGameBusy = busy;
|
||||
document.getElementById('start-game-btn').disabled = busy;
|
||||
document.getElementById('cancel-btn').disabled = busy;
|
||||
document.getElementById('computer-level').disabled = busy;
|
||||
document.getElementById('search-time').disabled = busy;
|
||||
document.getElementById('starting-fen').disabled = busy;
|
||||
document.querySelectorAll('input[name="player-color"]').forEach(r => r.disabled = busy);
|
||||
}
|
||||
|
||||
function showNewGameModal() {
|
||||
const modal = document.getElementById('modal-overlay');
|
||||
modal.classList.add('show');
|
||||
@@ -385,6 +476,8 @@ function hideNewGameModal() {
|
||||
const modal = document.getElementById('modal-overlay');
|
||||
modal.classList.remove('show');
|
||||
teardownModalKeyboardNav();
|
||||
clearModalMessage('new-game-modal-message');
|
||||
setNewGameModalBusy(false);
|
||||
}
|
||||
|
||||
function setupModalKeyboardNav() {
|
||||
@@ -399,6 +492,14 @@ function handleModalKeydown(e) {
|
||||
const modal = document.getElementById('modal-overlay');
|
||||
if (!modal.classList.contains('show')) return;
|
||||
|
||||
// While a request is in flight, block just Enter (re-submit) and Escape
|
||||
// (close); the color/level/time shortcuts fall through as no-ops since
|
||||
// those controls are disabled and there's nothing else bound to those keys.
|
||||
if (gameState.newGameBusy) {
|
||||
if (e.key === 'Enter' || e.key === 'Escape') e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
switch(e.key) {
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
@@ -474,14 +575,16 @@ function copyHistory() {
|
||||
}
|
||||
|
||||
async function startNewGame() {
|
||||
if (gameState.newGameBusy) return;
|
||||
|
||||
const playerColor = document.querySelector('input[name="player-color"]:checked').value;
|
||||
const computerLevel = parseInt(document.getElementById('computer-level').value);
|
||||
const searchTime = parseInt(document.getElementById('search-time').value);
|
||||
const startingFEN = document.getElementById('starting-fen').value.trim();
|
||||
gameState.isPlayerWhite = (playerColor === 'white');
|
||||
const willBePlayerWhite = (playerColor === 'white');
|
||||
|
||||
const whiteConfig = gameState.isPlayerWhite ? { type: 1 } : { type: 2, level: computerLevel, searchTime: searchTime };
|
||||
const blackConfig = gameState.isPlayerWhite ? { type: 2, level: computerLevel, searchTime: searchTime } : { type: 1 };
|
||||
const whiteConfig = willBePlayerWhite ? { type: 1 } : { type: 2, level: computerLevel, searchTime: searchTime };
|
||||
const blackConfig = willBePlayerWhite ? { type: 2, level: computerLevel, searchTime: searchTime } : { type: 1 };
|
||||
|
||||
const requestBody = {
|
||||
white: whiteConfig,
|
||||
@@ -493,34 +596,51 @@ async function startNewGame() {
|
||||
requestBody.fen = startingFEN;
|
||||
}
|
||||
|
||||
setNewGameModalBusy(true);
|
||||
clearModalMessage('new-game-modal-message');
|
||||
|
||||
let response;
|
||||
try {
|
||||
const response = await authFetch(`${gameState.apiUrl}/api/v1/games`, {
|
||||
response = await authFetch(`${gameState.apiUrl}/api/v1/games`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorInfo = handleApiError('create game', null, response);
|
||||
throw new Error(errorInfo.statusMessage);
|
||||
}
|
||||
|
||||
const game = await response.json();
|
||||
gameState.gameId = game.gameId;
|
||||
gameState.moveList = [];
|
||||
hideNewGameModal();
|
||||
initializeBoard();
|
||||
updateGameDisplay(game);
|
||||
document.getElementById('undo-btn').disabled = true;
|
||||
if (!gameState.isPlayerWhite) triggerComputerMove();
|
||||
|
||||
} catch (error) {
|
||||
if (error.message === 'Failed to fetch') {
|
||||
handleApiError('create game', error);
|
||||
} else {
|
||||
flashErrorMessage(error.message);
|
||||
}
|
||||
updateTurnIndicator('', '');
|
||||
const errorInfo = handleApiError('create game', error);
|
||||
setModalMessage('new-game-modal-message', errorInfo.statusMessage, 'error');
|
||||
setNewGameModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorInfo = handleApiError('create game', null, response);
|
||||
setModalMessage('new-game-modal-message', errorInfo.statusMessage, 'error');
|
||||
setNewGameModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let game;
|
||||
try {
|
||||
game = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Create game: response OK but JSON parse failed:', error);
|
||||
setModalMessage('new-game-modal-message', 'Unexpected response from server', 'error');
|
||||
setNewGameModalBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// isPlayerWhite is only committed to global state now that success is confirmed
|
||||
gameState.isPlayerWhite = willBePlayerWhite;
|
||||
gameState.gameId = game.gameId;
|
||||
gameState.moveList = [];
|
||||
initializeBoard();
|
||||
updateGameDisplay(game);
|
||||
document.getElementById('undo-btn').disabled = true;
|
||||
if (!gameState.isPlayerWhite) triggerComputerMove();
|
||||
|
||||
setModalMessage('new-game-modal-message', `Game started - you play ${willBePlayerWhite ? 'White' : 'Black'}`, 'success');
|
||||
setTimeout(hideNewGameModal, MODAL_SUCCESS_DISPLAY_MS);
|
||||
}
|
||||
|
||||
function initializeBoard() {
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
<button class="auth-tab" data-tab="register">Register</button>
|
||||
</div>
|
||||
|
||||
<div id="auth-modal-message" class="modal-message"></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<div id="login-form" class="auth-form">
|
||||
<div class="form-group">
|
||||
@@ -117,6 +119,7 @@
|
||||
<div id="modal-overlay" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<h2>New Game</h2>
|
||||
<div id="new-game-modal-message" class="modal-message"></div>
|
||||
<div class="form-group">
|
||||
<label class="group-label">Your Color</label>
|
||||
<div class="radio-group">
|
||||
|
||||
@@ -659,6 +659,53 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- Modal status message (Issue 2) --- */
|
||||
.modal-message {
|
||||
display: none;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-message.error {
|
||||
background: rgba(247, 118, 142, 0.12);
|
||||
color: var(--tokyo-red);
|
||||
border: 1px solid rgba(247, 118, 142, 0.4);
|
||||
}
|
||||
|
||||
.modal-message.success {
|
||||
background: rgba(158, 206, 106, 0.12);
|
||||
color: var(--tokyo-green);
|
||||
border: 1px solid rgba(158, 206, 106, 0.4);
|
||||
}
|
||||
|
||||
/* --- Disabled state while a modal request is in flight (Issue 1) --- */
|
||||
.auth-form input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-tab:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.fen-input:disabled,
|
||||
input[type="range"]:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.radio-group input:disabled + span {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Mobile/Responsiveness */
|
||||
@media (max-width: 978px) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user