Add user self-registration and implement rate limiting for predictions
Allow users to add themselves to the player database through a web form, eliminating the need for manual pdga-numbers.txt updates. Implement 24-hour rate limiting on prediction refreshes to prevent abuse while maintaining reasonable update frequency. Key changes: - Add player self-registration with PDGA number lookup and confirmation - Store predicted ratings in database for persistence across restarts - Implement 24-hour rate limit on prediction refresh endpoint - Make database the single source of truth (text file only for initial seed) - Remove "Scrape All Layouts" bulk operation button - Update "Load All" to refresh existing players instead of text file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+338
-14
@@ -311,6 +311,123 @@
|
||||
.debug-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
.add-player-section {
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.add-player-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
.add-player-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pdga-input {
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
width: 250px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.pdga-input:focus {
|
||||
border-color: #007bff;
|
||||
}
|
||||
.btn-add {
|
||||
background-color: #28a745;
|
||||
}
|
||||
.btn-add:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10001;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
.modal-header {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #495057;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.modal-footer {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
font-size: 28px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
}
|
||||
.modal-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
.btn-cancel {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
.btn-cancel:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
.btn-confirm {
|
||||
background-color: #28a745;
|
||||
}
|
||||
.btn-confirm:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.add-player-section {
|
||||
padding: 15px;
|
||||
}
|
||||
.add-player-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
.pdga-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -320,6 +437,23 @@
|
||||
<a href="/" style="margin: 0 15px; color: #333; text-decoration: none; font-weight: bold;">Player Ratings</a>
|
||||
<a href="/courses.html" style="margin: 0 15px; color: #007bff; text-decoration: none; font-weight: bold;">Courses</a>
|
||||
</div>
|
||||
|
||||
<!-- Add Player Section -->
|
||||
<div class="add-player-section">
|
||||
<h3>Add Yourself to Tracked Players</h3>
|
||||
<div class="add-player-form">
|
||||
<input
|
||||
type="number"
|
||||
id="pdga-number-input"
|
||||
class="pdga-input"
|
||||
placeholder="Enter your PDGA number"
|
||||
min="1"
|
||||
/>
|
||||
<button class="btn btn-add" onclick="searchAndAddPlayer()">
|
||||
<i class="fas fa-user-plus"></i> Add Player
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; top: 10px; right: 15px;">
|
||||
<a href="#" onclick="loadAllPlayers(); return false;" style="color: #007bff; font-size: 11px; text-decoration: none; margin-right: 10px;" title="Load all player data" id="load-all-btn">Load All</a>
|
||||
<a href="#" onclick="clearCache(); return false;" style="color: #ccc; font-size: 12px; text-decoration: none; opacity: 0.3;" title="Clear cache"><i class="fas fa-cog"></i></a>
|
||||
@@ -343,6 +477,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Player Confirmation Modal -->
|
||||
<div id="add-player-modal" class="modal" onclick="closeAddPlayerModal(event)">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<button class="modal-close" onclick="closeAddPlayerModal()">×</button>
|
||||
<div class="modal-header" id="add-player-modal-header">Confirm Player</div>
|
||||
<div class="modal-body" id="add-player-modal-body">Loading...</div>
|
||||
<div class="modal-footer" id="add-player-modal-footer">
|
||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Cancel</button>
|
||||
<button class="btn btn-confirm" id="confirm-add-btn" onclick="confirmAddPlayer()">Add Player</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fetchRatingsWithProgress() {
|
||||
const progressSection = document.getElementById('progress-section');
|
||||
@@ -774,31 +921,37 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing round history:', error);
|
||||
|
||||
// Try to get detailed error information
|
||||
|
||||
// Try to get detailed error information
|
||||
let errorMessage = 'Failed to refresh prediction data';
|
||||
let errorDetails = '';
|
||||
|
||||
|
||||
try {
|
||||
// If error message is JSON, parse it
|
||||
const errorData = JSON.parse(error.message);
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
errorDetails = errorData.details || '';
|
||||
if (errorData.suggestion) {
|
||||
errorDetails += '\n\nSuggestion: ' + errorData.suggestion;
|
||||
}
|
||||
if (errorData.errorType) {
|
||||
errorDetails += '\n\nError Type: ' + errorData.errorType;
|
||||
}
|
||||
if (errorData.timestamp) {
|
||||
errorDetails += '\n\nTime: ' + new Date(errorData.timestamp).toLocaleString();
|
||||
|
||||
// Special handling for rate limit errors
|
||||
if (errorData.message) {
|
||||
errorDetails = errorData.message;
|
||||
} else {
|
||||
errorDetails = errorData.details || '';
|
||||
if (errorData.suggestion) {
|
||||
errorDetails += '\n\nSuggestion: ' + errorData.suggestion;
|
||||
}
|
||||
if (errorData.errorType) {
|
||||
errorDetails += '\n\nError Type: ' + errorData.errorType;
|
||||
}
|
||||
if (errorData.timestamp) {
|
||||
errorDetails += '\n\nTime: ' + new Date(errorData.timestamp).toLocaleString();
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// If not JSON, just use the error message
|
||||
errorDetails = error.message;
|
||||
}
|
||||
|
||||
const fullMessage = errorDetails ? errorMessage + '\n\nDetails:\n' + errorDetails : errorMessage;
|
||||
|
||||
const fullMessage = errorDetails ? errorMessage + '\n\n' + errorDetails : errorMessage;
|
||||
alert(fullMessage);
|
||||
} finally {
|
||||
icon.classList.remove('spinning');
|
||||
@@ -970,6 +1123,177 @@
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add Player Functions
|
||||
let pendingPlayerData = null;
|
||||
|
||||
async function searchAndAddPlayer() {
|
||||
const input = document.getElementById('pdga-number-input');
|
||||
const pdgaNumber = input.value.trim();
|
||||
|
||||
if (!pdgaNumber) {
|
||||
alert('Please enter a PDGA number');
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.querySelector('.btn-add');
|
||||
const originalHTML = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<i class="fas fa-sync-alt spinning"></i> Searching...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search-player/${pdgaNumber}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
showErrorModal(data.error || 'Player not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.alreadyExists) {
|
||||
showInfoModal(`${data.player.name} is already being tracked!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store player data and show confirmation modal
|
||||
pendingPlayerData = data.player;
|
||||
showConfirmationModal(data.player);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error searching for player:', error);
|
||||
showErrorModal('Failed to search for player. Please try again.');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalHTML;
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirmationModal(player) {
|
||||
const modal = document.getElementById('add-player-modal');
|
||||
const header = document.getElementById('add-player-modal-header');
|
||||
const body = document.getElementById('add-player-modal-body');
|
||||
const footer = document.getElementById('add-player-modal-footer');
|
||||
|
||||
header.textContent = 'Confirm Player';
|
||||
body.innerHTML = `
|
||||
<p>Is this the correct player you want to add?</p>
|
||||
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;">
|
||||
<strong style="font-size: 18px; color: #007bff;">${player.name}</strong><br>
|
||||
<span style="color: #6c757d;">PDGA #${player.pdgaNumber}</span><br>
|
||||
${player.rating ? `<span style="color: #28a745; font-weight: bold;">Current Rating: ${player.rating}</span>` : '<span style="color: #999;">No rating available</span>'}
|
||||
</div>
|
||||
`;
|
||||
footer.innerHTML = `
|
||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Cancel</button>
|
||||
<button class="btn btn-confirm" onclick="confirmAddPlayer()">Add Player</button>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function showErrorModal(message) {
|
||||
const modal = document.getElementById('add-player-modal');
|
||||
const header = document.getElementById('add-player-modal-header');
|
||||
const body = document.getElementById('add-player-modal-body');
|
||||
const footer = document.getElementById('add-player-modal-footer');
|
||||
|
||||
header.textContent = 'Player Not Found';
|
||||
body.innerHTML = `
|
||||
<p style="color: #dc3545;">
|
||||
<i class="fas fa-exclamation-circle"></i> ${message}
|
||||
</p>
|
||||
<p style="margin-top: 10px; color: #6c757d; font-size: 14px;">
|
||||
Please check the PDGA number and try again.
|
||||
</p>
|
||||
`;
|
||||
footer.innerHTML = `
|
||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function showInfoModal(message) {
|
||||
const modal = document.getElementById('add-player-modal');
|
||||
const header = document.getElementById('add-player-modal-header');
|
||||
const body = document.getElementById('add-player-modal-body');
|
||||
const footer = document.getElementById('add-player-modal-footer');
|
||||
|
||||
header.textContent = 'Information';
|
||||
body.innerHTML = `
|
||||
<p style="color: #007bff;">
|
||||
<i class="fas fa-info-circle"></i> ${message}
|
||||
</p>
|
||||
`;
|
||||
footer.innerHTML = `
|
||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmAddPlayer() {
|
||||
if (!pendingPlayerData) {
|
||||
closeAddPlayerModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('add-player-modal');
|
||||
const body = document.getElementById('add-player-modal-body');
|
||||
const footer = document.getElementById('add-player-modal-footer');
|
||||
|
||||
// Show loading state
|
||||
body.innerHTML = '<p style="text-align: center;"><i class="fas fa-sync-alt spinning"></i> Adding player...</p>';
|
||||
footer.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/add-player', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to add player');
|
||||
}
|
||||
|
||||
// Success! Show success message
|
||||
body.innerHTML = `
|
||||
<p style="color: #28a745; text-align: center;">
|
||||
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 15px;"></i><br>
|
||||
<strong>${data.player.name}</strong> has been added successfully!
|
||||
</p>
|
||||
`;
|
||||
footer.innerHTML = `
|
||||
<button class="btn btn-confirm" onclick="closeAddPlayerModal(); location.reload();">OK</button>
|
||||
`;
|
||||
|
||||
// Clear input
|
||||
document.getElementById('pdga-number-input').value = '';
|
||||
pendingPlayerData = null;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding player:', error);
|
||||
body.innerHTML = `
|
||||
<p style="color: #dc3545;">
|
||||
<i class="fas fa-exclamation-circle"></i> ${error.message}
|
||||
</p>
|
||||
`;
|
||||
footer.innerHTML = `
|
||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddPlayerModal(event) {
|
||||
const modal = document.getElementById('add-player-modal');
|
||||
modal.style.display = 'none';
|
||||
pendingPlayerData = null;
|
||||
}
|
||||
|
||||
fetchRatingsWithProgress();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user