diff --git a/css/style.css b/css/style.css index b21e856..14e0f10 100644 --- a/css/style.css +++ b/css/style.css @@ -720,7 +720,110 @@ input:checked + .slider::before { transform: translateX(18px); } /* ── Utility ────────────────────────────────────────────────── */ .hidden { display: none !important; } -/* ── Responsive ─────────────────────────────────────────────── */ +/* ── SQL Input Section ──────────────────────────────────────── */ +.sql-input-section { + flex-shrink: 0; + border-top: 1px solid var(--bg-border); + background: var(--bg-surface); + display: flex; + flex-direction: column; + gap: 0; +} + +.sql-input-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: .4rem .75rem; + border-bottom: 1px solid var(--bg-border); +} + +.sql-input-title { + font-size: .75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--accent-green); +} + +.sql-input-hint { + font-size: .7rem; + color: var(--text-muted); +} + +.sql-move-input { + width: 100%; + background: var(--bg-base); + border: none; + border-bottom: 1px solid var(--bg-border); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: .78rem; + line-height: 1.6; + padding: .6rem .75rem; + resize: none; + outline: none; + transition: background .15s; +} +.sql-move-input:focus { + background: var(--bg-base); +} +.sql-move-input::placeholder { color: var(--text-muted); } + +.sql-input-actions { + display: flex; + align-items: center; + gap: .4rem; + padding: .4rem .6rem; + justify-content: flex-end; +} + +.sql-run-error { + flex: 1; + font-size: .75rem; + color: var(--accent-red); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Copy button on SQL blocks ──────────────────────────────── */ +.sql-block-label { position: relative; } + +.sql-copy-btn { + position: absolute; + right: .5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: 1px solid transparent; + color: var(--text-muted); + font-size: .65rem; + padding: .1rem .35rem; + border-radius: var(--radius); + cursor: pointer; + transition: color .15s, border-color .15s, background .15s; + font-family: var(--font-sans); +} +.sql-copy-btn:hover { + color: var(--text-secondary); + border-color: var(--bg-border); + background: var(--bg-elevated); +} + +/* ── Active-turn glow on player bar ─────────────────────────── */ +.player-bar.active-turn { + border-color: var(--accent-green); + box-shadow: 0 0 0 1px var(--accent-green), inset 0 0 8px rgba(63,185,80,.07); + transition: border-color .3s, box-shadow .3s; +} + +/* ── Responsive additions ───────────────────────────────────── */ +@media (max-width: 900px) { + .sql-input-section { display: flex; } +} + @media (max-width: 900px) { .app-main { flex-direction: column; } diff --git a/index.html b/index.html index b0f057f..2f32340 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,25 @@

Each chess move is translated into
real SQL statements in real time.

+ + +
+
+ ▶ Execute SQL Move + Ctrl ↵ to run +
+ +
+ + + +
+
diff --git a/js/app.js b/js/app.js index 00d8d61..53b4efb 100644 --- a/js/app.js +++ b/js/app.js @@ -42,6 +42,7 @@ const state = { pendingPromotion: null, // { from, to } while waiting for user to pick capturedByWhite: [], // pieces taken by white (black pieces lost) capturedByBlack: [], // pieces taken by black (white pieces lost) + sqlInputHasTemplate: false, // true while the SQL textarea shows an auto-filled template }; /* ─── Helpers ─────────────────────────────────────────────────── */ @@ -170,6 +171,8 @@ function updatePlayerBars() { document.getElementById('blackBadge').textContent = turn === 'b' ? 'Your turn' : ''; document.getElementById('whiteBar').style.opacity = turn === 'w' ? '1' : '.65'; document.getElementById('blackBar').style.opacity = turn === 'b' ? '1' : '.65'; + document.getElementById('whiteBar').classList.toggle('active-turn', turn === 'w'); + document.getElementById('blackBar').classList.toggle('active-turn', turn === 'b'); } function updateStatus() { @@ -222,6 +225,123 @@ function renderAll() { buildMoveHistory(); } +/* ─── SQL Move Input ──────────────────────────────────────────── */ + +/** + * Populate the SQL input textarea with a move template for the selected piece. + */ +function fillSQLInputTemplate(sqName, piece) { + const input = document.getElementById('sqlMoveInput'); + if (!input) return; + const color = piece.color === 'w' ? 'white' : 'black'; + input.value = + `UPDATE chess_piece\n` + + `SET position = '???'\n` + + `WHERE position = '${sqName}'\n` + + ` AND color = '${color}';`; + state.sqlInputHasTemplate = true; + // Place cursor on the ??? so the user can immediately type the destination + const idx = input.value.indexOf('???'); + input.focus(); + input.setSelectionRange(idx, idx + 3); + clearSQLRunError(); +} + +/** + * Parse a SQL string and extract { from, to } squares. + * Accepted formats: + * 1. UPDATE chess_piece SET position = 'e4' WHERE ... position = 'e2' ... + * 2. Shorthand: e2 e4 / e2-e4 / e2 to e4 + */ +function parseSQLMove(query) { + const q = query.trim(); + + // Shorthand: "e2 e4", "e2-e4", "e2 to e4", "e2e4" + const shorthand = q.match(/^([a-h][1-8])\s*(?:-|to)?\s*([a-h][1-8])$/i); + if (shorthand) { + const from = shorthand[1].toLowerCase(); + const to = shorthand[2].toLowerCase(); + if (from === to) return null; + return { from, to }; + } + + // Standard UPDATE … SET position = 'to' … WHERE … position = 'from' + const setMatch = q.match(/SET\s+position\s*=\s*'([a-h][1-8])'/i); + if (!setMatch) return null; + const to = setMatch[1].toLowerCase(); + + const whereIdx = q.search(/WHERE/i); + if (whereIdx === -1) return null; + const whereClause = q.slice(whereIdx); + const fromMatch = whereClause.match(/position\s*=\s*'([a-h][1-8])'/i); + if (!fromMatch) return null; + const from = fromMatch[1].toLowerCase(); + + if (from === to) return null; + return { from, to }; +} + +function showSQLRunError(msg) { + const el = document.getElementById('sqlRunError'); + if (!el) return; + el.textContent = msg; + el.classList.remove('hidden'); +} + +function clearSQLRunError() { + const el = document.getElementById('sqlRunError'); + if (el) { el.textContent = ''; el.classList.add('hidden'); } +} + +function runSQLMove() { + if (!state.chess) { showSQLRunError('No game in progress.'); return; } + if (state.chess.game_over()) { showSQLRunError('Game is over.'); return; } + if (state.pendingPromotion) { showSQLRunError('Finish pawn promotion first.'); return; } + + const input = document.getElementById('sqlMoveInput'); + const query = input ? input.value : ''; + if (!query.trim()) { showSQLRunError('Enter a SQL statement to move a piece.'); return; } + + const parsed = parseSQLMove(query); + if (!parsed) { + showSQLRunError('Could not parse move. Use: UPDATE chess_piece SET position = \'e4\' WHERE position = \'e2\';'); + return; + } + + const { from, to } = parsed; + + // Validate that there's a piece at `from` belonging to the current player + const piece = state.chess.get(from); + if (!piece) { showSQLRunError(`No piece found at ${from}.`); return; } + if (piece.color !== state.chess.turn()) { + showSQLRunError(`It is ${state.chess.turn() === 'w' ? 'White' : 'Black'}'s turn.`); + return; + } + + // Check if it's a promotion + const validMoves = state.chess.moves({ square: from, verbose: true }); + const moveObj = validMoves.find(m => m.to === to); + if (!moveObj) { + showSQLRunError(`Move ${from}→${to} is not legal.`); + return; + } + + clearSQLRunError(); + if (input) input.value = ''; + + // Reset board selection + state.selectedSquare = null; + state.validMoves = []; + + if (moveObj.flags.includes('p')) { + state.pendingPromotion = { from, to }; + openPromotionDialog(state.chess.turn()); + return; + } + + executeMove(from, to, null); +} + /* ─── Square Click Logic ──────────────────────────────────────── */ function onSquareClick(sqName) { // Ignore clicks if game over or awaiting promotion @@ -237,6 +357,8 @@ function onSquareClick(sqName) { state.selectedSquare = sqName; state.validMoves = state.chess.moves({ square: sqName, verbose: true }); renderPieces(); + // Auto-fill SQL input with template + fillSQLInputTemplate(sqName, piece); return; } @@ -298,6 +420,14 @@ function executeMove(from, to, promotion) { state.validMoves = []; state.moveCount++; + // Clear SQL input template after a successful board-click move + const sqlMoveInput = document.getElementById('sqlMoveInput'); + if (sqlMoveInput && state.sqlInputHasTemplate) { + sqlMoveInput.value = ''; + state.sqlInputHasTemplate = false; + clearSQLRunError(); + } + // Generate SQL if (state.showSQL) { const sql = SQLGen.move(moveResult, state.moveCount, state.gameId); @@ -621,6 +751,15 @@ function appendSQL(code, label, moveNum) { } labelEl.append(' ' + label); + // Per-block copy button + const copyBtn = document.createElement('button'); + copyBtn.type = 'button'; + copyBtn.className = 'sql-copy-btn'; + copyBtn.textContent = '⎘ Copy'; + copyBtn.title = 'Copy this SQL block'; + copyBtn.addEventListener('click', () => copyToClipboard(code, 'SQL copied!')); + labelEl.appendChild(copyBtn); + const codeEl = document.createElement('div'); codeEl.className = 'sql-code'; codeEl.innerHTML = highlightSQL(code); @@ -651,11 +790,17 @@ function startGame(whiteName, blackName, showSQL, existingPGN) { state.capturedByWhite = []; state.capturedByBlack = []; state.pendingPromotion = null; + state.sqlInputHasTemplate = false; // Names in UI document.getElementById('whitePlayerName').textContent = state.whitePlayer; document.getElementById('blackPlayerName').textContent = state.blackPlayer; + // Clear SQL input + const sqlMoveInput = document.getElementById('sqlMoveInput'); + if (sqlMoveInput) sqlMoveInput.value = ''; + clearSQLRunError(); + // SQL panel visibility const sqlPanel = document.getElementById('sqlPanel'); const lblEl = document.getElementById('sqlToggleLabel'); @@ -873,6 +1018,21 @@ function init() { }); // Close invite on overlay click already wired above + + // SQL Move Input + document.getElementById('btnRunSQL').addEventListener('click', runSQLMove); + document.getElementById('btnClearInput').addEventListener('click', () => { + const input = document.getElementById('sqlMoveInput'); + if (input) input.value = ''; + state.sqlInputHasTemplate = false; + clearSQLRunError(); + }); + document.getElementById('sqlMoveInput').addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + runSQLMove(); + } + }); } function copyToClipboard(text, toastMsg) {