Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
19 changes: 19 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ <h2 class="sql-title">
<p class="placeholder-hint">Each chess move is translated into<br>real SQL statements in real time.</p>
</div>
</div>

<!-- ── SQL Move Input ──────────────────────────────────── -->
<div class="sql-input-section" id="sqlInputSection">
<div class="sql-input-bar">
<span class="sql-input-title">▶ Execute SQL Move</span>
<span class="sql-input-hint" title="Press Ctrl+Enter to run">Ctrl ↵ to run</span>
</div>
<textarea
id="sqlMoveInput"
class="sql-move-input"
spellcheck="false"
rows="4"
placeholder="UPDATE chess_piece&#10;SET position = 'e4'&#10;WHERE position = 'e2';&#10;&#10;-- or shorthand: e2 e4"></textarea>
<div class="sql-input-actions">
<span class="sql-run-error hidden" id="sqlRunError"></span>
<button type="button" id="btnClearInput" class="btn btn-xs">Clear</button>
<button type="button" id="btnRunSQL" class="btn btn-primary btn-sm">▶ Run</button>
</div>
</div>
</section>

</main>
Expand Down
160 changes: 160 additions & 0 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────── */
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down
Loading