Refactor: JS in js/, LESS in less/, clientseitige Less-Kompilierung via less.js CDN
This commit is contained in:
633
js/app.js
Normal file
633
js/app.js
Normal file
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
* Lara Kiesewetter – Live Schachturnier
|
||||
* Haupt-Application
|
||||
*/
|
||||
|
||||
/* global $, parsePGN, filterLaraGames, getLiveGame, getLatestGame, Chess, Chessboard */
|
||||
|
||||
let board = null;
|
||||
let chess = null;
|
||||
let currentGame = null;
|
||||
let allLaraGames = [];
|
||||
let currentMoveIndex = -1;
|
||||
let userSelectedGame = false;
|
||||
let userScrolledMoves = false;
|
||||
let currentRound = 0;
|
||||
let roundPgns = {};
|
||||
let pollId = 0;
|
||||
|
||||
async function fetchRoundPGN(round) {
|
||||
const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn?t=${Date.now()}`);
|
||||
if (!res.ok) return null;
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
async function loadPGN(showOverlay = true) {
|
||||
const currentPollId = pollId;
|
||||
if (showOverlay) showLoading(true);
|
||||
hideError();
|
||||
|
||||
try {
|
||||
await updateStandings();
|
||||
|
||||
if (currentRound === 0) {
|
||||
if (showOverlay) showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Runden immer frisch von der DSJ laden (mit Cache-Busting)
|
||||
const maxRound = currentRound + 1;
|
||||
for (let r = 1; r <= maxRound; r++) {
|
||||
const text = await fetchRoundPGN(r);
|
||||
if (currentPollId !== pollId) return;
|
||||
if (text !== null) roundPgns[r] = text;
|
||||
}
|
||||
|
||||
// Alle PGNs kombinieren
|
||||
const combinedPgn = Object.values(roundPgns).join('\n\n');
|
||||
const allGames = parsePGN(combinedPgn);
|
||||
allLaraGames = filterLaraGames(allGames);
|
||||
|
||||
if (allLaraGames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userSelectedGame) {
|
||||
const liveGame = getLiveGame(allLaraGames);
|
||||
const newGame = liveGame || getLatestGame(allLaraGames);
|
||||
if (newGame !== currentGame) {
|
||||
userScrolledMoves = false;
|
||||
}
|
||||
currentGame = newGame;
|
||||
}
|
||||
updateBoard();
|
||||
updatePlayerInfo();
|
||||
updateMovesList();
|
||||
updateAllGamesList();
|
||||
updatePGNDisplay();
|
||||
updateTimestamp();
|
||||
|
||||
showLoading(false);
|
||||
|
||||
} catch (error) {
|
||||
if (currentPollId !== pollId) return;
|
||||
console.error('Fehler beim Laden:', error);
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Schachbrett
|
||||
*/
|
||||
function updateBoard() {
|
||||
if (!currentGame) return;
|
||||
|
||||
chess = new Chess();
|
||||
|
||||
// Spiegele das Brett, wenn Lara Schwarz hat
|
||||
const laraIsBlack = currentGame.black.toLowerCase().includes('kiesewetter');
|
||||
const orientation = laraIsBlack ? 'black' : 'white';
|
||||
|
||||
// Führe alle Züge aus
|
||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||
for (const move of nonResultMoves) {
|
||||
try {
|
||||
chess.move(move.san);
|
||||
} catch {
|
||||
// Ignoriere ungültige Züge
|
||||
}
|
||||
}
|
||||
if (!userScrolledMoves) {
|
||||
currentMoveIndex = nonResultMoves.length - 1;
|
||||
} else if (currentMoveIndex >= 0) {
|
||||
// Benutzer hat navigiert – zeige Brett an seinem ausgewählten Zug
|
||||
chess = new Chess();
|
||||
for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) {
|
||||
try {
|
||||
chess.move(nonResultMoves[i].san);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (board) {
|
||||
board.position(chess.fen(), true);
|
||||
board.orientation(orientation);
|
||||
board.resize();
|
||||
highlightLastMove();
|
||||
} else {
|
||||
board = Chessboard('board', {
|
||||
position: chess.fen(),
|
||||
orientation: orientation,
|
||||
pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png',
|
||||
draggable: false,
|
||||
spawnMoveError: false
|
||||
});
|
||||
|
||||
// Click auf Züge
|
||||
document.getElementById('moves-list').addEventListener('click', handleMoveClick);
|
||||
}
|
||||
|
||||
// Highlight active player
|
||||
highlightActivePlayer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gehe zu einem bestimmten Zug (Index)
|
||||
*/
|
||||
function goToMove(index) {
|
||||
if (!currentGame) return;
|
||||
|
||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||
|
||||
if (index < -1) index = -1;
|
||||
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
|
||||
|
||||
userScrolledMoves = index < nonResultMoves.length - 1;
|
||||
currentMoveIndex = index;
|
||||
|
||||
chess = new Chess();
|
||||
for (let i = 0; i <= index; i++) {
|
||||
try {
|
||||
chess.move(nonResultMoves[i].san);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
board.position(chess.fen(), true);
|
||||
highlightActivePlayer();
|
||||
highlightLastMove();
|
||||
updateClocks(index);
|
||||
|
||||
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
|
||||
if (index >= 0) {
|
||||
const moveEl = document.querySelector(`#moves-list [data-index="${index}"]`);
|
||||
if (moveEl) moveEl.classList.add('current');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Spielerinformationen
|
||||
*/
|
||||
function updatePlayerInfo() {
|
||||
if (!currentGame) return;
|
||||
|
||||
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
|
||||
|
||||
const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black;
|
||||
const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo;
|
||||
const laraEmoji = laraIsWhite ? '⬜' : '⬛';
|
||||
|
||||
const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white;
|
||||
const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo;
|
||||
const oppEmoji = laraIsWhite ? '⬛' : '⬜';
|
||||
|
||||
// Top-Panel (player-black) = Gegner
|
||||
document.getElementById('black-name').textContent = oppPlayer;
|
||||
document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : '';
|
||||
document.querySelector('#player-black .player-avatar').textContent = oppEmoji;
|
||||
|
||||
// Bottom-Panel (player-white) = Lara
|
||||
document.getElementById('white-name').textContent = laraPlayer;
|
||||
document.getElementById('white-elo').textContent = laraElo ? `(ELO: ${laraElo})` : '';
|
||||
document.querySelector('#player-white .player-avatar').textContent = laraEmoji;
|
||||
|
||||
updateClocks(currentMoveIndex);
|
||||
|
||||
// Round info
|
||||
const roundInfo = `Runde ${currentGame.round} – ${currentGame.event || 'Turnier'}`;
|
||||
document.getElementById('round-info').textContent = roundInfo;
|
||||
const mobileRound = document.getElementById('round-info-mobile');
|
||||
if (mobileRound) mobileRound.textContent = roundInfo;
|
||||
|
||||
// Result info
|
||||
const resultEl = document.getElementById('result-info');
|
||||
if (currentGame.isLive) {
|
||||
resultEl.innerHTML = '<span style="color: #4ade80;">● Laufen</span>';
|
||||
} else {
|
||||
resultEl.textContent = `Ergebnis: ${currentGame.result}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Uhrenanzeige basierend auf dem aktuellen Zugindex
|
||||
*/
|
||||
function updateClocks(moveIndex) {
|
||||
if (!currentGame) return;
|
||||
|
||||
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
|
||||
|
||||
let whiteClock, blackClock;
|
||||
|
||||
if (moveIndex < 0) {
|
||||
whiteClock = currentGame.whiteClock;
|
||||
blackClock = currentGame.blackClock;
|
||||
} else {
|
||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||
if (moveIndex < nonResultMoves.length) {
|
||||
whiteClock = nonResultMoves[moveIndex].whiteClock;
|
||||
blackClock = nonResultMoves[moveIndex].blackClock;
|
||||
}
|
||||
}
|
||||
|
||||
const laraClock = laraIsWhite ? whiteClock : blackClock;
|
||||
const oppClock = laraIsWhite ? blackClock : whiteClock;
|
||||
|
||||
document.getElementById('black-clock').textContent = formatClock(oppClock);
|
||||
document.getElementById('white-clock').textContent = formatClock(laraClock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight den Spieler, der gerade am Zug ist
|
||||
*/
|
||||
function highlightActivePlayer() {
|
||||
if (!chess || !currentGame) return;
|
||||
|
||||
const laraPanel = document.getElementById('player-white');
|
||||
const oppPanel = document.getElementById('player-black');
|
||||
|
||||
laraPanel.classList.remove('active');
|
||||
oppPanel.classList.remove('active');
|
||||
|
||||
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
|
||||
const isLaraTurn = (chess.turn() === 'w' && laraIsWhite) || (chess.turn() === 'b' && !laraIsWhite);
|
||||
|
||||
if (isLaraTurn) {
|
||||
laraPanel.classList.add('active');
|
||||
} else {
|
||||
oppPanel.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight den letzten Zug auf dem Brett
|
||||
*/
|
||||
function highlightLastMove() {
|
||||
if (!board || !chess || !currentGame) return;
|
||||
|
||||
$('#board [data-square]').removeClass('last-move-highlight');
|
||||
|
||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||
|
||||
if (currentMoveIndex >= 0 && currentMoveIndex < nonResultMoves.length) {
|
||||
const moves = chess.history({ verbose: true });
|
||||
if (moves.length > 0) {
|
||||
const lastMoveData = moves[moves.length - 1];
|
||||
$(`#board [data-square="${lastMoveData.from}"]`).addClass('last-move-highlight');
|
||||
$(`#board [data-square="${lastMoveData.to}"]`).addClass('last-move-highlight');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert PGN-Text aus einem Spiel-Objekt
|
||||
*/
|
||||
function generatePGN(game) {
|
||||
if (!game) return '';
|
||||
|
||||
let pgn = '';
|
||||
if (game.event) pgn += `[Event "${game.event}"]\n`;
|
||||
if (game.site) pgn += `[Site "${game.site}"]\n`;
|
||||
if (game.date) pgn += `[Date "${game.date}"]\n`;
|
||||
if (game.round) pgn += `[Round "${game.round}"]\n`;
|
||||
if (game.white) pgn += `[White "${game.white}"]\n`;
|
||||
if (game.black) pgn += `[Black "${game.black}"]\n`;
|
||||
if (game.result) pgn += `[Result "${game.result}"]\n`;
|
||||
if (game.whiteElo) pgn += `[WhiteElo "${game.whiteElo}"]\n`;
|
||||
if (game.blackElo) pgn += `[BlackElo "${game.blackElo}"]\n`;
|
||||
if (game.termination) pgn += `[Termination "${game.termination}"]\n`;
|
||||
|
||||
pgn += '\n';
|
||||
|
||||
const nonResultMoves = game.moves.filter(m => !m.isResult);
|
||||
for (let i = 0; i < nonResultMoves.length; i += 2) {
|
||||
const moveNumber = Math.floor(i / 2) + 1;
|
||||
pgn += `${moveNumber}. ${nonResultMoves[i].san}`;
|
||||
if (i + 1 < nonResultMoves.length) {
|
||||
pgn += ` ${nonResultMoves[i + 1].san} `;
|
||||
}
|
||||
}
|
||||
|
||||
const resultMove = game.moves.find(m => m.isResult);
|
||||
if (resultMove) {
|
||||
pgn += ` ${resultMove.san}`;
|
||||
}
|
||||
|
||||
return pgn.trim();
|
||||
}
|
||||
|
||||
function updatePGNDisplay() {
|
||||
if (!currentGame) return;
|
||||
const text = generatePGN(currentGame);
|
||||
document.querySelectorAll('.pgn-text').forEach(el => el.textContent = text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Zugliste
|
||||
*/
|
||||
function updateMovesList() {
|
||||
if (!currentGame) return;
|
||||
|
||||
const movesList = document.getElementById('moves-list');
|
||||
movesList.innerHTML = '';
|
||||
|
||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
|
||||
|
||||
for (let i = 0; i < nonResultMoves.length; i += 2) {
|
||||
const moveNumber = Math.floor(i / 2) + 1;
|
||||
|
||||
const numSpan = document.createElement('span');
|
||||
numSpan.className = 'move-number';
|
||||
numSpan.textContent = `${moveNumber}.`;
|
||||
movesList.appendChild(numSpan);
|
||||
|
||||
if (laraIsWhite) {
|
||||
// Lara (Weiß) zuerst
|
||||
const laraMove = document.createElement('span');
|
||||
laraMove.className = 'move lara-move';
|
||||
laraMove.textContent = nonResultMoves[i].san;
|
||||
laraMove.dataset.index = i;
|
||||
movesList.appendChild(laraMove);
|
||||
|
||||
if (i + 1 < nonResultMoves.length) {
|
||||
const oppMove = document.createElement('span');
|
||||
oppMove.className = 'move opp-move';
|
||||
oppMove.textContent = nonResultMoves[i + 1].san;
|
||||
oppMove.dataset.index = i + 1;
|
||||
movesList.appendChild(oppMove);
|
||||
}
|
||||
} else {
|
||||
// Lara (Schwarz) – Gegner zuerst, dann Lara
|
||||
const oppMove = document.createElement('span');
|
||||
oppMove.className = 'move opp-move';
|
||||
oppMove.textContent = nonResultMoves[i].san;
|
||||
oppMove.dataset.index = i;
|
||||
movesList.appendChild(oppMove);
|
||||
|
||||
if (i + 1 < nonResultMoves.length) {
|
||||
const laraMove = document.createElement('span');
|
||||
laraMove.className = 'move lara-move';
|
||||
laraMove.textContent = nonResultMoves[i + 1].san;
|
||||
laraMove.dataset.index = i + 1;
|
||||
movesList.appendChild(laraMove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark current move
|
||||
if (currentMoveIndex >= 0) {
|
||||
const curMove = movesList.querySelector(`[data-index="${currentMoveIndex}"]`);
|
||||
if (curMove) curMove.classList.add('current');
|
||||
}
|
||||
|
||||
// Scroll to bottom nur wenn Benutzer nicht manuell navigiert hat
|
||||
if (!userScrolledMoves) {
|
||||
movesList.scrollTop = movesList.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Klick auf einen Zug in der Liste
|
||||
*/
|
||||
function handleMoveClick(e) {
|
||||
if (!e.target.classList.contains('move') || !currentGame) return;
|
||||
goToMove(parseInt(e.target.dataset.index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Liste aller Partien
|
||||
*/
|
||||
function updateAllGamesList() {
|
||||
const list = document.getElementById('all-games-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
// Sort by round
|
||||
const sorted = [...allLaraGames].sort((a, b) => {
|
||||
return (parseInt(a.round) || 0) - (parseInt(b.round) || 0);
|
||||
});
|
||||
|
||||
for (const game of sorted) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'game-entry';
|
||||
if (currentGame && game === currentGame) entry.classList.add('active');
|
||||
|
||||
const laraIsWhite = game.white.toLowerCase().includes('kiesewetter');
|
||||
const opponent = laraIsWhite ? game.black : game.white;
|
||||
const color = laraIsWhite ? '⬜ Weiß' : '⬛ Schwarz';
|
||||
|
||||
entry.innerHTML = `
|
||||
<div class="game-round">Runde ${game.round}</div>
|
||||
<div class="game-players">Lara ${color} vs ${opponent}</div>
|
||||
<div class="game-result">${game.isLive ? '● Laufen' : game.result}</div>
|
||||
`;
|
||||
|
||||
entry.addEventListener('click', () => {
|
||||
userSelectedGame = true;
|
||||
userScrolledMoves = false;
|
||||
currentGame = game;
|
||||
updateBoard();
|
||||
updatePlayerInfo();
|
||||
updateMovesList();
|
||||
updateAllGamesList();
|
||||
updatePGNDisplay();
|
||||
});
|
||||
|
||||
list.appendChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Turniertabelle vom DSJ und zeigt Laras Platzierung an
|
||||
*/
|
||||
async function updateStandings() {
|
||||
try {
|
||||
const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/tabelle/?t=${Date.now()}`);
|
||||
if (!res.ok) throw new Error('Fehler beim Laden');
|
||||
const html = await res.text();
|
||||
|
||||
const roundMatch = html.match(/Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde/);
|
||||
if (roundMatch) {
|
||||
currentRound = parseInt(roundMatch[1]);
|
||||
}
|
||||
|
||||
const rows = html.matchAll(/<tr[^>]*>(.*?)<\/tr>/gs);
|
||||
for (const row of rows) {
|
||||
if (!row[1].includes('Lara Kiesewetter')) continue;
|
||||
|
||||
const cells = row[1].matchAll(/<td[^>]*>(.*?)<\/td>/gs);
|
||||
const clean = [];
|
||||
for (const cell of cells) {
|
||||
clean.push(cell[1].replace(/<[^>]+>/g, '').trim());
|
||||
}
|
||||
|
||||
if (clean.length >= 9) {
|
||||
const data = {
|
||||
rank: clean[0],
|
||||
player: 'Lara Kiesewetter',
|
||||
wins: clean[5],
|
||||
draws: clean[6],
|
||||
losses: clean[7],
|
||||
points: clean[8],
|
||||
round_info: roundMatch ? `nach der ${roundMatch[1]}. Runde` : '',
|
||||
round: currentRound,
|
||||
};
|
||||
const container = document.getElementById('standings-content');
|
||||
if (!data || data.error) {
|
||||
container.innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<div class="standings-rank">${data.rank}.</div>
|
||||
<div class="standings-rank-label">Tabellenplatz</div>
|
||||
<div class="standings-header">${data.round_info || 'nach Runde 1'}</div>
|
||||
<div class="standings-row">
|
||||
<span class="standings-label">Punkte</span>
|
||||
<span class="standings-value">${data.points}</span>
|
||||
</div>
|
||||
<div class="standings-row">
|
||||
<span class="standings-label">Siege</span>
|
||||
<span class="standings-value">${data.wins}</span>
|
||||
</div>
|
||||
<div class="standings-row">
|
||||
<span class="standings-label">Unentschieden</span>
|
||||
<span class="standings-value">${data.draws}</span>
|
||||
</div>
|
||||
<div class="standings-row">
|
||||
<span class="standings-label">Niederlagen</span>
|
||||
<span class="standings-value">${data.losses}</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.getElementById('standings-content').innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
|
||||
} catch {
|
||||
document.getElementById('standings-content').innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format clock string
|
||||
*/
|
||||
function formatClock(clockStr) {
|
||||
if (!clockStr) return '--:--:--';
|
||||
// Format is HH:MM:SS
|
||||
return clockStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update timestamp
|
||||
*/
|
||||
function updateTimestamp() {
|
||||
const time = new Date();
|
||||
document.getElementById('last-update').textContent =
|
||||
`Letztes Update: ${time.toLocaleTimeString('de-DE')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet Auto-Refresh alle 30 Sekunden
|
||||
*/
|
||||
function startAutoRefresh() {
|
||||
const myId = ++pollId;
|
||||
let lastUpdate = Date.now();
|
||||
let pollInterval, timer;
|
||||
|
||||
document.getElementById('refresh-timer').textContent = '0s';
|
||||
document.getElementById('refresh-timer').style.color = '#4ade80';
|
||||
|
||||
loadPGN(true);
|
||||
|
||||
pollInterval = setInterval(() => {
|
||||
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
|
||||
loadPGN(false);
|
||||
lastUpdate = Date.now();
|
||||
}, 30000);
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
|
||||
const elapsed = Date.now() - lastUpdate;
|
||||
const remaining = Math.max(0, 30000 - elapsed);
|
||||
const s = Math.floor(remaining / 1000);
|
||||
document.getElementById('refresh-timer').textContent = `${s}s`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Helpers
|
||||
*/
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
document.getElementById('error-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual refresh button – startet neuen Long-Poll-Zyklus
|
||||
*/
|
||||
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||||
pollId++;
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
/**
|
||||
* PGN in Zwischenablage kopieren
|
||||
*/
|
||||
document.querySelectorAll('.copy-pgn-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const panel = btn.closest('[class*="pgn-panel"]');
|
||||
const text = panel.querySelector('.pgn-text').textContent;
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
btn.textContent = '✅';
|
||||
setTimeout(() => { btn.textContent = '📋'; }, 1500);
|
||||
} catch {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Pfeiltasten-Navigation
|
||||
*/
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!currentGame) return;
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
goToMove(currentMoveIndex - 1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
goToMove(currentMoveIndex + 1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Board-Resize bei Fenster- und Container-Änderungen
|
||||
*/
|
||||
window.addEventListener('resize', () => {
|
||||
if (board) board.resize();
|
||||
});
|
||||
|
||||
if (window.ResizeObserver) {
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (board) board.resize();
|
||||
});
|
||||
ro.observe(document.getElementById('board'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*/
|
||||
loadPGN();
|
||||
startAutoRefresh();
|
||||
137
js/pgn-parser.js
Normal file
137
js/pgn-parser.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* PGN Parser - Parses PGN files and extracts game data
|
||||
*/
|
||||
|
||||
function parsePGN(pgnText) {
|
||||
const games = [];
|
||||
// Split by game boundaries - each game starts with [Event
|
||||
const gameBlocks = pgnText.split(/\[\s*Event\s*"/);
|
||||
|
||||
for (let i = 1; i < gameBlocks.length; i++) {
|
||||
const game = parseGameBlock(gameBlocks[i]);
|
||||
if (game) games.push(game);
|
||||
}
|
||||
|
||||
return games;
|
||||
}
|
||||
|
||||
function parseGameBlock(block) {
|
||||
try {
|
||||
const headers = {};
|
||||
|
||||
// Der Event-Header fehlt, weil wir danach splitten – extrahiere ihn aus dem Blockanfang
|
||||
const eventEnd = block.indexOf('"]');
|
||||
if (eventEnd > 1) {
|
||||
headers.Event = block.substring(1, eventEnd);
|
||||
}
|
||||
|
||||
const headerRegex = /^\s*\[(\w+)\s+"([^"]*)"\]/gm;
|
||||
let match;
|
||||
|
||||
// Extract all headers
|
||||
let tempBlock = block;
|
||||
while ((match = headerRegex.exec(tempBlock)) !== null) {
|
||||
headers[match[1]] = match[2];
|
||||
}
|
||||
|
||||
// Extract moves - everything after the last header
|
||||
const lastHeaderEnd = block.lastIndexOf('"]');
|
||||
let movesText = lastHeaderEnd > -1 ? block.substring(lastHeaderEnd + 2).trim() : '';
|
||||
|
||||
// Remove comments from moves for cleaner parsing
|
||||
const moves = parseMoves(movesText);
|
||||
|
||||
return {
|
||||
event: headers.Event || '',
|
||||
site: headers.Site || '',
|
||||
date: headers.Date || '',
|
||||
round: headers.Round || '',
|
||||
white: headers.White || '',
|
||||
black: headers.Black || '',
|
||||
result: headers.Result || '',
|
||||
termination: headers.Termination || '',
|
||||
whiteElo: headers.WhiteElo || '',
|
||||
blackElo: headers.BlackElo || '',
|
||||
whiteClock: headers.WhiteClock || '',
|
||||
blackClock: headers.BlackClock || '',
|
||||
moves: moves,
|
||||
isLive: headers.Termination === 'unterminated' || headers.Result === '*'
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error parsing game:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseMoves(movesText) {
|
||||
const moves = [];
|
||||
|
||||
let clockWhite = null;
|
||||
let clockBlack = null;
|
||||
let color = 'w';
|
||||
|
||||
const tokenRegex = /\d+\.\s*|\{[^}]*\}|\S+/g;
|
||||
const tokens = movesText.match(tokenRegex) || [];
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let token = tokens[i].trim();
|
||||
|
||||
if (['1-0', '0-1', '1/2-1/2', '*'].includes(token)) {
|
||||
moves.push({ san: token, isResult: true, whiteClock: clockWhite, blackClock: clockBlack });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\d+\.$/.test(token)) continue;
|
||||
|
||||
if (token.startsWith('{')) {
|
||||
const clkMatch = token.match(/\[%clk\s+([\d:]+)\]/);
|
||||
if (clkMatch && moves.length > 0) {
|
||||
const lastMove = moves[moves.length - 1];
|
||||
if (!lastMove.isResult && lastMove.color) {
|
||||
if (lastMove.color === 'w') {
|
||||
clockWhite = clkMatch[1];
|
||||
} else {
|
||||
clockBlack = clkMatch[1];
|
||||
}
|
||||
lastMove.whiteClock = clockWhite;
|
||||
lastMove.blackClock = clockBlack;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const move = { san: token, isResult: false, whiteClock: clockWhite, blackClock: clockBlack, color };
|
||||
moves.push(move);
|
||||
color = color === 'w' ? 'b' : 'w';
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
|
||||
function filterLaraGames(games) {
|
||||
return games.filter(game =>
|
||||
game.white.toLowerCase().includes('kiesewetter') ||
|
||||
game.black.toLowerCase().includes('kiesewetter')
|
||||
);
|
||||
}
|
||||
|
||||
function getLiveGame(laraGames) {
|
||||
// Return the game that is still in progress
|
||||
return laraGames.find(game => game.isLive) || null;
|
||||
}
|
||||
|
||||
function getLatestGame(laraGames) {
|
||||
if (laraGames.length === 0) return null;
|
||||
// Sort by round number, return the highest round
|
||||
const sorted = [...laraGames].sort((a, b) => {
|
||||
const roundA = parseInt(a.round) || 0;
|
||||
const roundB = parseInt(b.round) || 0;
|
||||
return roundB - roundA;
|
||||
});
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
window.parsePGN = parsePGN;
|
||||
window.filterLaraGames = filterLaraGames;
|
||||
window.getLiveGame = getLiveGame;
|
||||
window.getLatestGame = getLatestGame;
|
||||
Reference in New Issue
Block a user