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:
Samuel Enocsson
2025-08-12 17:22:12 +02:00
parent 2994f221f7
commit ef3881a0ac
2 changed files with 155 additions and 19 deletions
+103 -15
View File
@@ -7,9 +7,16 @@
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
}
.container {
max-width: 800px;
margin: 0 auto;
@@ -17,12 +24,28 @@
padding: 30px;
border-radius: 8px;
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 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
@media (max-width: 768px) {
h1 {
font-size: 24px;
margin-bottom: 20px;
}
}
.loading {
text-align: center;
padding: 20px;
@@ -57,12 +80,60 @@
width: 100%;
border-collapse: collapse;
margin-top: 20px;
font-size: 14px;
}
th, td {
padding: 12px;
text-align: left;
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 {
background-color: #f8f9fa;
font-weight: bold;
@@ -148,6 +219,17 @@
border-radius: 3px;
cursor: pointer;
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 {
background-color: #138496;
@@ -245,13 +327,13 @@
<table>
<thead>
<tr>
<th>Rank</th>
<th class="mobile-hide">Rank</th>
<th>Player Name</th>
<th>PDGA #</th>
<th>Current Rating</th>
<th>Rating Change</th>
<th>Predicted Rating</th>
<th>Difference</th>
<th class="mobile-hide">PDGA #</th>
<th>Rating</th>
<th class="mobile-hide">Change</th>
<th class="mobile-hide">Predicted</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@@ -270,21 +352,27 @@
tableHTML += `
<tr id="row-${player.pdgaNumber}" class="expandable-row" onclick="togglePlayerHistory(${player.pdgaNumber})">
<td>${index + 1}</td>
<td><a href="https://www.pdga.com/player/${player.pdgaNumber}" target="_blank" onclick="event.stopPropagation()">${player.name}</a></td>
<td class="pdga-number">#${player.pdgaNumber}</td>
<td class="rating">${player.rating || 'N/A'}</td>
<td class="rating-change ${ratingChangeClass}">${ratingChangeText}</td>
<td class="predicted-rating" id="predicted-${player.pdgaNumber}">
<td class="mobile-hide">${index + 1}</td>
<td class="player-name">
<a href="https://www.pdga.com/player/${player.pdgaNumber}" target="_blank" onclick="event.stopPropagation()">${player.name}</a>
<div class="mobile-only pdga-number" style="font-size: 11px; color: #999; margin-top: 2px;">PDGA #${player.pdgaNumber}</div>
</td>
<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'}
</td>
<td class="difference ${diffClass}" id="diff-${player.pdgaNumber}">
${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>
</tr>
<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-container" id="chart-${player.pdgaNumber}" style="position: relative;">
<div class="loading-chart">Click to load rating history...</div>
+52 -4
View File
@@ -165,7 +165,11 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
const url = `https://www.pdga.com/player/${pdgaNumber}`;
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 urls = [];
@@ -185,7 +189,7 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (date > oneYearAgo) {
if (date > oneYearAgo && date < nextUpdateDate) {
const href = tournamentCell.getAttribute('href');
if (href) {
urls.push({
@@ -200,7 +204,7 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) {
});
return urls.slice(0, 8); // Reduce number of tournaments to scrape
});
}, nextUpdateDate.getTime());
console.log(`Found ${tournamentUrls.length} recent tournaments for PDGA ${pdgaNumber}`);
@@ -287,11 +291,55 @@ function parseDate(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) {
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
.filter(r => r.date < nextUpdateDate) // Only include rounds before next update
.sort((a, b) => b.date - a.date)
.map(r => r.rating)
.filter(r => r > 0);