Add mobile optimization and PDGA update date simulation
Mobile improvements: - Responsive table layout with hidden columns on mobile - Touch-friendly buttons and improved spacing - Consolidated information display for small screens - Mobile-specific CSS with media queries PDGA rating simulation: - Calculate next official PDGA update date (2nd Tuesday of each month) - Filter tournaments to only include rounds before next update - Simulate realistic rating predictions based on PDGA schedule - Account for rolling 12-month window and round expiration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+103
-15
@@ -7,9 +7,16 @@
|
|||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
margin: 40px;
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -17,12 +24,28 @@
|
|||||||
padding: 30px;
|
padding: 30px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -57,12 +80,60 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
th, td {
|
th, td {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide less important columns on mobile */
|
||||||
|
.mobile-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show mobile-only elements only on mobile */
|
||||||
|
.mobile-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make player names more prominent on mobile */
|
||||||
|
.player-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide mobile-only elements on desktop */
|
||||||
|
.mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Adjust colspan for mobile - fewer visible columns */
|
||||||
|
.expanded-cell {
|
||||||
|
/* Mobile shows: Player Name, Rating, Action = 3 columns */
|
||||||
|
/* But we need to account for the fact that mobile-hide columns still exist in DOM */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better mobile chart sizing */
|
||||||
|
.chart-container {
|
||||||
|
height: 250px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
th {
|
th {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -148,6 +219,17 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
min-height: 32px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.calc-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.calc-btn:hover {
|
.calc-btn:hover {
|
||||||
background-color: #138496;
|
background-color: #138496;
|
||||||
@@ -245,13 +327,13 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Rank</th>
|
<th class="mobile-hide">Rank</th>
|
||||||
<th>Player Name</th>
|
<th>Player Name</th>
|
||||||
<th>PDGA #</th>
|
<th class="mobile-hide">PDGA #</th>
|
||||||
<th>Current Rating</th>
|
<th>Rating</th>
|
||||||
<th>Rating Change</th>
|
<th class="mobile-hide">Change</th>
|
||||||
<th>Predicted Rating</th>
|
<th class="mobile-hide">Predicted</th>
|
||||||
<th>Difference</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -270,21 +352,27 @@
|
|||||||
|
|
||||||
tableHTML += `
|
tableHTML += `
|
||||||
<tr id="row-${player.pdgaNumber}" class="expandable-row" onclick="togglePlayerHistory(${player.pdgaNumber})">
|
<tr id="row-${player.pdgaNumber}" class="expandable-row" onclick="togglePlayerHistory(${player.pdgaNumber})">
|
||||||
<td>${index + 1}</td>
|
<td class="mobile-hide">${index + 1}</td>
|
||||||
<td><a href="https://www.pdga.com/player/${player.pdgaNumber}" target="_blank" onclick="event.stopPropagation()">${player.name}</a></td>
|
<td class="player-name">
|
||||||
<td class="pdga-number">#${player.pdgaNumber}</td>
|
<a href="https://www.pdga.com/player/${player.pdgaNumber}" target="_blank" onclick="event.stopPropagation()">${player.name}</a>
|
||||||
<td class="rating">${player.rating || 'N/A'}</td>
|
<div class="mobile-only pdga-number" style="font-size: 11px; color: #999; margin-top: 2px;">PDGA #${player.pdgaNumber}</div>
|
||||||
<td class="rating-change ${ratingChangeClass}">${ratingChangeText}</td>
|
</td>
|
||||||
<td class="predicted-rating" id="predicted-${player.pdgaNumber}">
|
<td class="pdga-number mobile-hide">#${player.pdgaNumber}</td>
|
||||||
|
<td class="rating">
|
||||||
|
${player.rating || 'N/A'}
|
||||||
|
<div class="mobile-only rating-change ${ratingChangeClass}" style="font-size: 11px; margin-top: 2px;">${ratingChangeText}</div>
|
||||||
|
</td>
|
||||||
|
<td class="rating-change ${ratingChangeClass} mobile-hide">${ratingChangeText}</td>
|
||||||
|
<td class="predicted-rating mobile-hide" id="predicted-${player.pdgaNumber}">
|
||||||
${player.predictedRating || 'N/A'}
|
${player.predictedRating || 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
<td class="difference ${diffClass}" id="diff-${player.pdgaNumber}">
|
<td class="difference ${diffClass}" id="diff-${player.pdgaNumber}">
|
||||||
${difference ? diffText :
|
${difference ? diffText :
|
||||||
`<button class="calc-btn" onclick="event.stopPropagation(); calculatePredictedRating(${player.pdgaNumber})">Predict Rating</button>`}
|
`<button class="calc-btn" onclick="event.stopPropagation(); calculatePredictedRating(${player.pdgaNumber})">Predict</button>`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="history-${player.pdgaNumber}" class="expanded-content">
|
<tr id="history-${player.pdgaNumber}" class="expanded-content">
|
||||||
<td colspan="7">
|
<td colspan="7" class="expanded-cell">
|
||||||
<div class="chart-title">Rating History for ${player.name}</div>
|
<div class="chart-title">Rating History for ${player.name}</div>
|
||||||
<div class="chart-container" id="chart-${player.pdgaNumber}" style="position: relative;">
|
<div class="chart-container" id="chart-${player.pdgaNumber}" style="position: relative;">
|
||||||
<div class="loading-chart">Click to load rating history...</div>
|
<div class="loading-chart">Click to load rating history...</div>
|
||||||
|
|||||||
@@ -165,7 +165,11 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
|
|||||||
const url = `https://www.pdga.com/player/${pdgaNumber}`;
|
const url = `https://www.pdga.com/player/${pdgaNumber}`;
|
||||||
await page.goto(url, { waitUntil: 'networkidle2' });
|
await page.goto(url, { waitUntil: 'networkidle2' });
|
||||||
|
|
||||||
const tournamentUrls = await page.evaluate(() => {
|
// Calculate the next PDGA update date to filter tournaments
|
||||||
|
const nextUpdateDate = getNextPDGAUpdateDate();
|
||||||
|
|
||||||
|
const tournamentUrls = await page.evaluate((nextUpdateTimestamp) => {
|
||||||
|
const nextUpdateDate = new Date(nextUpdateTimestamp);
|
||||||
const tables = document.querySelectorAll('table[id*="player-results"]');
|
const tables = document.querySelectorAll('table[id*="player-results"]');
|
||||||
const urls = [];
|
const urls = [];
|
||||||
|
|
||||||
@@ -185,7 +189,7 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
|
|||||||
const oneYearAgo = new Date();
|
const oneYearAgo = new Date();
|
||||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||||
|
|
||||||
if (date > oneYearAgo) {
|
if (date > oneYearAgo && date < nextUpdateDate) {
|
||||||
const href = tournamentCell.getAttribute('href');
|
const href = tournamentCell.getAttribute('href');
|
||||||
if (href) {
|
if (href) {
|
||||||
urls.push({
|
urls.push({
|
||||||
@@ -200,7 +204,7 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return urls.slice(0, 8); // Reduce number of tournaments to scrape
|
return urls.slice(0, 8); // Reduce number of tournaments to scrape
|
||||||
});
|
}, nextUpdateDate.getTime());
|
||||||
|
|
||||||
console.log(`Found ${tournamentUrls.length} recent tournaments for PDGA ${pdgaNumber}`);
|
console.log(`Found ${tournamentUrls.length} recent tournaments for PDGA ${pdgaNumber}`);
|
||||||
|
|
||||||
@@ -287,11 +291,55 @@ function parseDate(dateStr) {
|
|||||||
return new Date(dateStr);
|
return new Date(dateStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNextPDGAUpdateDate() {
|
||||||
|
const today = new Date();
|
||||||
|
const currentMonth = today.getMonth();
|
||||||
|
const currentYear = today.getFullYear();
|
||||||
|
|
||||||
|
// Calculate 2nd Tuesday of current month
|
||||||
|
const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
|
||||||
|
const firstTuesday = new Date(firstDayOfMonth);
|
||||||
|
|
||||||
|
// Find first Tuesday (day 2 = Tuesday, 0 = Sunday)
|
||||||
|
const daysUntilTuesday = (2 - firstDayOfMonth.getDay() + 7) % 7;
|
||||||
|
firstTuesday.setDate(1 + daysUntilTuesday);
|
||||||
|
|
||||||
|
// Second Tuesday is 7 days after first Tuesday
|
||||||
|
const secondTuesday = new Date(firstTuesday);
|
||||||
|
secondTuesday.setDate(firstTuesday.getDate() + 7);
|
||||||
|
|
||||||
|
// If today is before or on the 2nd Tuesday of this month, use this month's date
|
||||||
|
// Otherwise, use next month's 2nd Tuesday
|
||||||
|
if (today <= secondTuesday) {
|
||||||
|
return secondTuesday;
|
||||||
|
} else {
|
||||||
|
// Calculate 2nd Tuesday of next month
|
||||||
|
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
|
||||||
|
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
|
||||||
|
|
||||||
|
const firstDayNextMonth = new Date(nextYear, nextMonth, 1);
|
||||||
|
const firstTuesdayNext = new Date(firstDayNextMonth);
|
||||||
|
|
||||||
|
const daysUntilTuesdayNext = (2 - firstDayNextMonth.getDay() + 7) % 7;
|
||||||
|
firstTuesdayNext.setDate(1 + daysUntilTuesdayNext);
|
||||||
|
|
||||||
|
const secondTuesdayNext = new Date(firstTuesdayNext);
|
||||||
|
secondTuesdayNext.setDate(firstTuesdayNext.getDate() + 7);
|
||||||
|
|
||||||
|
return secondTuesdayNext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function calculatePredictedRating(roundRatings) {
|
function calculatePredictedRating(roundRatings) {
|
||||||
if (!roundRatings || roundRatings.length === 0) return 0;
|
if (!roundRatings || roundRatings.length === 0) return 0;
|
||||||
|
|
||||||
// Sort by date (most recent first) and extract ratings
|
// Get the next PDGA rating update date - only include rounds before this date
|
||||||
|
const nextUpdateDate = getNextPDGAUpdateDate();
|
||||||
|
console.log(`Next PDGA update date: ${nextUpdateDate.toDateString()}`);
|
||||||
|
|
||||||
|
// Filter rounds to only include those before the next update date, then sort by date (most recent first) and extract ratings
|
||||||
const sortedRatings = roundRatings
|
const sortedRatings = roundRatings
|
||||||
|
.filter(r => r.date < nextUpdateDate) // Only include rounds before next update
|
||||||
.sort((a, b) => b.date - a.date)
|
.sort((a, b) => b.date - a.date)
|
||||||
.map(r => r.rating)
|
.map(r => r.rating)
|
||||||
.filter(r => r > 0);
|
.filter(r => r > 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user