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.
+
+
+
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) {