Skip to content

Commit 5683590

Browse files
authored
Merge pull request #2 from Devn913/copilot/improve-ui-ux
feat: move chess pieces by typing SQL queries + UI polish
2 parents dbf6dbc + ae16ab2 commit 5683590

3 files changed

Lines changed: 283 additions & 1 deletion

File tree

css/style.css

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,110 @@ input:checked + .slider::before { transform: translateX(18px); }
720720
/* ── Utility ────────────────────────────────────────────────── */
721721
.hidden { display: none !important; }
722722

723-
/* ── Responsive ─────────────────────────────────────────────── */
723+
/* ── SQL Input Section ──────────────────────────────────────── */
724+
.sql-input-section {
725+
flex-shrink: 0;
726+
border-top: 1px solid var(--bg-border);
727+
background: var(--bg-surface);
728+
display: flex;
729+
flex-direction: column;
730+
gap: 0;
731+
}
732+
733+
.sql-input-bar {
734+
display: flex;
735+
align-items: center;
736+
justify-content: space-between;
737+
padding: .4rem .75rem;
738+
border-bottom: 1px solid var(--bg-border);
739+
}
740+
741+
.sql-input-title {
742+
font-size: .75rem;
743+
font-weight: 700;
744+
text-transform: uppercase;
745+
letter-spacing: .05em;
746+
color: var(--accent-green);
747+
}
748+
749+
.sql-input-hint {
750+
font-size: .7rem;
751+
color: var(--text-muted);
752+
}
753+
754+
.sql-move-input {
755+
width: 100%;
756+
background: var(--bg-base);
757+
border: none;
758+
border-bottom: 1px solid var(--bg-border);
759+
color: var(--text-primary);
760+
font-family: var(--font-mono);
761+
font-size: .78rem;
762+
line-height: 1.6;
763+
padding: .6rem .75rem;
764+
resize: none;
765+
outline: none;
766+
transition: background .15s;
767+
}
768+
.sql-move-input:focus {
769+
background: var(--bg-base);
770+
}
771+
.sql-move-input::placeholder { color: var(--text-muted); }
772+
773+
.sql-input-actions {
774+
display: flex;
775+
align-items: center;
776+
gap: .4rem;
777+
padding: .4rem .6rem;
778+
justify-content: flex-end;
779+
}
780+
781+
.sql-run-error {
782+
flex: 1;
783+
font-size: .75rem;
784+
color: var(--accent-red);
785+
font-weight: 500;
786+
white-space: nowrap;
787+
overflow: hidden;
788+
text-overflow: ellipsis;
789+
}
790+
791+
/* ── Copy button on SQL blocks ──────────────────────────────── */
792+
.sql-block-label { position: relative; }
793+
794+
.sql-copy-btn {
795+
position: absolute;
796+
right: .5rem;
797+
top: 50%;
798+
transform: translateY(-50%);
799+
background: none;
800+
border: 1px solid transparent;
801+
color: var(--text-muted);
802+
font-size: .65rem;
803+
padding: .1rem .35rem;
804+
border-radius: var(--radius);
805+
cursor: pointer;
806+
transition: color .15s, border-color .15s, background .15s;
807+
font-family: var(--font-sans);
808+
}
809+
.sql-copy-btn:hover {
810+
color: var(--text-secondary);
811+
border-color: var(--bg-border);
812+
background: var(--bg-elevated);
813+
}
814+
815+
/* ── Active-turn glow on player bar ─────────────────────────── */
816+
.player-bar.active-turn {
817+
border-color: var(--accent-green);
818+
box-shadow: 0 0 0 1px var(--accent-green), inset 0 0 8px rgba(63,185,80,.07);
819+
transition: border-color .3s, box-shadow .3s;
820+
}
821+
822+
/* ── Responsive additions ───────────────────────────────────── */
823+
@media (max-width: 900px) {
824+
.sql-input-section { display: flex; }
825+
}
826+
724827
@media (max-width: 900px) {
725828
.app-main { flex-direction: column; }
726829

index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ <h2 class="sql-title">
9797
<p class="placeholder-hint">Each chess move is translated into<br>real SQL statements in real time.</p>
9898
</div>
9999
</div>
100+
101+
<!-- ── SQL Move Input ──────────────────────────────────── -->
102+
<div class="sql-input-section" id="sqlInputSection">
103+
<div class="sql-input-bar">
104+
<span class="sql-input-title">▶ Execute SQL Move</span>
105+
<span class="sql-input-hint" title="Press Ctrl+Enter to run">Ctrl ↵ to run</span>
106+
</div>
107+
<textarea
108+
id="sqlMoveInput"
109+
class="sql-move-input"
110+
spellcheck="false"
111+
rows="4"
112+
placeholder="UPDATE chess_piece&#10;SET position = 'e4'&#10;WHERE position = 'e2';&#10;&#10;-- or shorthand: e2 e4"></textarea>
113+
<div class="sql-input-actions">
114+
<span class="sql-run-error hidden" id="sqlRunError"></span>
115+
<button type="button" id="btnClearInput" class="btn btn-xs">Clear</button>
116+
<button type="button" id="btnRunSQL" class="btn btn-primary btn-sm">▶ Run</button>
117+
</div>
118+
</div>
100119
</section>
101120

102121
</main>

js/app.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const state = {
4242
pendingPromotion: null, // { from, to } while waiting for user to pick
4343
capturedByWhite: [], // pieces taken by white (black pieces lost)
4444
capturedByBlack: [], // pieces taken by black (white pieces lost)
45+
sqlInputHasTemplate: false, // true while the SQL textarea shows an auto-filled template
4546
};
4647

4748
/* ─── Helpers ─────────────────────────────────────────────────── */
@@ -170,6 +171,8 @@ function updatePlayerBars() {
170171
document.getElementById('blackBadge').textContent = turn === 'b' ? 'Your turn' : '';
171172
document.getElementById('whiteBar').style.opacity = turn === 'w' ? '1' : '.65';
172173
document.getElementById('blackBar').style.opacity = turn === 'b' ? '1' : '.65';
174+
document.getElementById('whiteBar').classList.toggle('active-turn', turn === 'w');
175+
document.getElementById('blackBar').classList.toggle('active-turn', turn === 'b');
173176
}
174177

175178
function updateStatus() {
@@ -222,6 +225,123 @@ function renderAll() {
222225
buildMoveHistory();
223226
}
224227

228+
/* ─── SQL Move Input ──────────────────────────────────────────── */
229+
230+
/**
231+
* Populate the SQL input textarea with a move template for the selected piece.
232+
*/
233+
function fillSQLInputTemplate(sqName, piece) {
234+
const input = document.getElementById('sqlMoveInput');
235+
if (!input) return;
236+
const color = piece.color === 'w' ? 'white' : 'black';
237+
input.value =
238+
`UPDATE chess_piece\n` +
239+
`SET position = '???'\n` +
240+
`WHERE position = '${sqName}'\n` +
241+
` AND color = '${color}';`;
242+
state.sqlInputHasTemplate = true;
243+
// Place cursor on the ??? so the user can immediately type the destination
244+
const idx = input.value.indexOf('???');
245+
input.focus();
246+
input.setSelectionRange(idx, idx + 3);
247+
clearSQLRunError();
248+
}
249+
250+
/**
251+
* Parse a SQL string and extract { from, to } squares.
252+
* Accepted formats:
253+
* 1. UPDATE chess_piece SET position = 'e4' WHERE ... position = 'e2' ...
254+
* 2. Shorthand: e2 e4 / e2-e4 / e2 to e4
255+
*/
256+
function parseSQLMove(query) {
257+
const q = query.trim();
258+
259+
// Shorthand: "e2 e4", "e2-e4", "e2 to e4", "e2e4"
260+
const shorthand = q.match(/^([a-h][1-8])\s*(?:-|to)?\s*([a-h][1-8])$/i);
261+
if (shorthand) {
262+
const from = shorthand[1].toLowerCase();
263+
const to = shorthand[2].toLowerCase();
264+
if (from === to) return null;
265+
return { from, to };
266+
}
267+
268+
// Standard UPDATE … SET position = 'to' … WHERE … position = 'from'
269+
const setMatch = q.match(/SET\s+position\s*=\s*'([a-h][1-8])'/i);
270+
if (!setMatch) return null;
271+
const to = setMatch[1].toLowerCase();
272+
273+
const whereIdx = q.search(/WHERE/i);
274+
if (whereIdx === -1) return null;
275+
const whereClause = q.slice(whereIdx);
276+
const fromMatch = whereClause.match(/position\s*=\s*'([a-h][1-8])'/i);
277+
if (!fromMatch) return null;
278+
const from = fromMatch[1].toLowerCase();
279+
280+
if (from === to) return null;
281+
return { from, to };
282+
}
283+
284+
function showSQLRunError(msg) {
285+
const el = document.getElementById('sqlRunError');
286+
if (!el) return;
287+
el.textContent = msg;
288+
el.classList.remove('hidden');
289+
}
290+
291+
function clearSQLRunError() {
292+
const el = document.getElementById('sqlRunError');
293+
if (el) { el.textContent = ''; el.classList.add('hidden'); }
294+
}
295+
296+
function runSQLMove() {
297+
if (!state.chess) { showSQLRunError('No game in progress.'); return; }
298+
if (state.chess.game_over()) { showSQLRunError('Game is over.'); return; }
299+
if (state.pendingPromotion) { showSQLRunError('Finish pawn promotion first.'); return; }
300+
301+
const input = document.getElementById('sqlMoveInput');
302+
const query = input ? input.value : '';
303+
if (!query.trim()) { showSQLRunError('Enter a SQL statement to move a piece.'); return; }
304+
305+
const parsed = parseSQLMove(query);
306+
if (!parsed) {
307+
showSQLRunError('Could not parse move. Use: UPDATE chess_piece SET position = \'e4\' WHERE position = \'e2\';');
308+
return;
309+
}
310+
311+
const { from, to } = parsed;
312+
313+
// Validate that there's a piece at `from` belonging to the current player
314+
const piece = state.chess.get(from);
315+
if (!piece) { showSQLRunError(`No piece found at ${from}.`); return; }
316+
if (piece.color !== state.chess.turn()) {
317+
showSQLRunError(`It is ${state.chess.turn() === 'w' ? 'White' : 'Black'}'s turn.`);
318+
return;
319+
}
320+
321+
// Check if it's a promotion
322+
const validMoves = state.chess.moves({ square: from, verbose: true });
323+
const moveObj = validMoves.find(m => m.to === to);
324+
if (!moveObj) {
325+
showSQLRunError(`Move ${from}${to} is not legal.`);
326+
return;
327+
}
328+
329+
clearSQLRunError();
330+
if (input) input.value = '';
331+
332+
// Reset board selection
333+
state.selectedSquare = null;
334+
state.validMoves = [];
335+
336+
if (moveObj.flags.includes('p')) {
337+
state.pendingPromotion = { from, to };
338+
openPromotionDialog(state.chess.turn());
339+
return;
340+
}
341+
342+
executeMove(from, to, null);
343+
}
344+
225345
/* ─── Square Click Logic ──────────────────────────────────────── */
226346
function onSquareClick(sqName) {
227347
// Ignore clicks if game over or awaiting promotion
@@ -237,6 +357,8 @@ function onSquareClick(sqName) {
237357
state.selectedSquare = sqName;
238358
state.validMoves = state.chess.moves({ square: sqName, verbose: true });
239359
renderPieces();
360+
// Auto-fill SQL input with template
361+
fillSQLInputTemplate(sqName, piece);
240362
return;
241363
}
242364

@@ -298,6 +420,14 @@ function executeMove(from, to, promotion) {
298420
state.validMoves = [];
299421
state.moveCount++;
300422

423+
// Clear SQL input template after a successful board-click move
424+
const sqlMoveInput = document.getElementById('sqlMoveInput');
425+
if (sqlMoveInput && state.sqlInputHasTemplate) {
426+
sqlMoveInput.value = '';
427+
state.sqlInputHasTemplate = false;
428+
clearSQLRunError();
429+
}
430+
301431
// Generate SQL
302432
if (state.showSQL) {
303433
const sql = SQLGen.move(moveResult, state.moveCount, state.gameId);
@@ -621,6 +751,15 @@ function appendSQL(code, label, moveNum) {
621751
}
622752
labelEl.append(' ' + label);
623753

754+
// Per-block copy button
755+
const copyBtn = document.createElement('button');
756+
copyBtn.type = 'button';
757+
copyBtn.className = 'sql-copy-btn';
758+
copyBtn.textContent = '⎘ Copy';
759+
copyBtn.title = 'Copy this SQL block';
760+
copyBtn.addEventListener('click', () => copyToClipboard(code, 'SQL copied!'));
761+
labelEl.appendChild(copyBtn);
762+
624763
const codeEl = document.createElement('div');
625764
codeEl.className = 'sql-code';
626765
codeEl.innerHTML = highlightSQL(code);
@@ -651,11 +790,17 @@ function startGame(whiteName, blackName, showSQL, existingPGN) {
651790
state.capturedByWhite = [];
652791
state.capturedByBlack = [];
653792
state.pendingPromotion = null;
793+
state.sqlInputHasTemplate = false;
654794

655795
// Names in UI
656796
document.getElementById('whitePlayerName').textContent = state.whitePlayer;
657797
document.getElementById('blackPlayerName').textContent = state.blackPlayer;
658798

799+
// Clear SQL input
800+
const sqlMoveInput = document.getElementById('sqlMoveInput');
801+
if (sqlMoveInput) sqlMoveInput.value = '';
802+
clearSQLRunError();
803+
659804
// SQL panel visibility
660805
const sqlPanel = document.getElementById('sqlPanel');
661806
const lblEl = document.getElementById('sqlToggleLabel');
@@ -873,6 +1018,21 @@ function init() {
8731018
});
8741019

8751020
// Close invite on overlay click already wired above
1021+
1022+
// SQL Move Input
1023+
document.getElementById('btnRunSQL').addEventListener('click', runSQLMove);
1024+
document.getElementById('btnClearInput').addEventListener('click', () => {
1025+
const input = document.getElementById('sqlMoveInput');
1026+
if (input) input.value = '';
1027+
state.sqlInputHasTemplate = false;
1028+
clearSQLRunError();
1029+
});
1030+
document.getElementById('sqlMoveInput').addEventListener('keydown', (e) => {
1031+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
1032+
e.preventDefault();
1033+
runSQLMove();
1034+
}
1035+
});
8761036
}
8771037

8781038
function copyToClipboard(text, toastMsg) {

0 commit comments

Comments
 (0)