feat: add target rating calculator (#2)

This commit is contained in:
Samuel Enocsson
2026-05-22 13:15:16 +02:00
parent c6ac174921
commit 1e66b9f94f
6 changed files with 343 additions and 0 deletions
+55
View File
@@ -298,3 +298,58 @@
font-size: 13px;
}
}
/* ── Target Rating Calculator ─────────────────── */
.target-rating-icon {
color: var(--accent);
}
#target-rating-form .form-row {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
#target-rating-form .form-row label {
font-size: 0.9em;
}
#target-rating-form .form-row input {
padding: 6px 8px;
}
#target-rating-form .form-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.target-rating-result {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.target-rating-result .target-summary div {
margin-bottom: 6px;
}
.target-rating-result .warning {
color: var(--down);
margin-top: 8px;
}
.target-rating-result .error {
color: var(--red);
}
.target-rating-result .muted {
color: var(--text-muted);
font-size: 0.9em;
}
.target-rating-result .loading {
color: var(--text-muted);
}
+121
View File
@@ -540,3 +540,124 @@ document.addEventListener('keydown', function(e) {
const pdgaNumber = row.id.replace('row-', '');
togglePlayerHistory(parseInt(pdgaNumber, 10));
});
// ── Target Rating Calculator ───────────────────────
function openTargetRatingModal(pdgaNumber) {
const modal = document.getElementById('target-rating-modal');
const header = document.getElementById('target-rating-modal-header');
const pdgaField = document.getElementById('target-rating-pdga');
const result = document.getElementById('target-rating-result');
const targetInput = document.getElementById('target-rating-input');
const roundsInput = document.getElementById('target-rounds-input');
const submitBtn = document.getElementById('target-rating-submit');
const playerNameEl = document.querySelector('#row-' + pdgaNumber + ' .player-name a');
const playerName = playerNameEl ? playerNameEl.textContent : 'PDGA #' + pdgaNumber;
header.textContent = 'Calculate Target Rating — ' + playerName;
pdgaField.value = pdgaNumber;
result.style.display = 'none';
while (result.firstChild) result.removeChild(result.firstChild);
targetInput.value = '';
roundsInput.value = '4';
submitBtn.disabled = false;
submitBtn.textContent = 'Calculate';
modal.style.display = 'flex';
targetInput.focus();
}
function _targetResultMsg(parent, cls, text) {
const d = document.createElement('div');
d.className = cls;
d.textContent = text;
parent.appendChild(d);
}
async function calculateTargetRating(event) {
if (event) event.preventDefault();
const pdgaNumber = document.getElementById('target-rating-pdga').value;
const targetRating = parseInt(document.getElementById('target-rating-input').value, 10);
const rounds = parseInt(document.getElementById('target-rounds-input').value, 10);
const result = document.getElementById('target-rating-result');
const submitBtn = document.getElementById('target-rating-submit');
function clearResult() {
while (result.firstChild) result.removeChild(result.firstChild);
}
if (!Number.isInteger(targetRating) || targetRating < 400 || targetRating > 1200) {
result.style.display = 'block';
clearResult();
_targetResultMsg(result, 'error', 'Target rating must be 400-1200.');
return;
}
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 20) {
result.style.display = 'block';
clearResult();
_targetResultMsg(result, 'error', 'Rounds must be an integer 1-20.');
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'Calculating...';
result.style.display = 'block';
clearResult();
_targetResultMsg(result, 'loading', 'Calculating...');
try {
const response = await fetch('/api/calculate-target-rating/' + pdgaNumber, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetRating: targetRating, rounds: rounds })
});
const data = await response.json();
clearResult();
if (!response.ok || !data.success) {
const msg = data.details ? data.error + ': ' + data.details : (data.error || 'Calculation failed');
_targetResultMsg(result, 'error', msg);
return;
}
const summary = document.createElement('div');
summary.className = 'target-summary';
const avgLine = document.createElement('div');
const avgStrong = document.createElement('strong');
avgStrong.textContent = 'Required round average: ';
avgLine.appendChild(avgStrong);
avgLine.appendChild(document.createTextNode(String(data.requiredAverage)));
summary.appendChild(avgLine);
const currentLine = document.createElement('div');
currentLine.textContent = 'Current predicted rating: ' + data.currentRating;
summary.appendChild(currentLine);
const simLine = document.createElement('div');
simLine.textContent = 'Simulated predicted rating at this average: ' + data.predictedRating;
summary.appendChild(simLine);
const mutedLine = document.createElement('div');
mutedLine.className = 'muted';
mutedLine.textContent = 'Across ' + data.rounds + ' synthetic rounds before the next PDGA update.';
summary.appendChild(mutedLine);
if (data.warning) {
_targetResultMsg(summary, 'warning', data.warning);
}
result.appendChild(summary);
} catch (err) {
console.error('Error calculating target rating:', err);
clearResult();
_targetResultMsg(result, 'error', 'Network error. Please try again.');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Calculate';
}
}
function closeTargetRatingModal(event) {
document.getElementById('target-rating-modal').style.display = 'none';
}