Add calendar template and schedule generation for MLB schedule

- Added a calendar template using Handlebars.js to generate a schedule for the Major League Baseball (MLB) games.
- Implemented a function to fetch and parse the MLB schedule API and populate the calendar with game details.
- Enhanced the calendar layout with custom CSS to improve readability and visual appeal.
- Added support for team-specific schedule display based on the user's favorite team ID.
- Implemented a feature to mark specific games with a special annotation based on specific criteria.
- Included support for different sheet sizes (letter, A4) and customized the header and calendar layout accordingly.
- Added a header image based on the user's favorite team ID.
- Enhanced the user experience by displaying game details like opponent, time, and annotation (e.g., A or B for home and away games).
- Improved the overall layout and design to enhance the schedule readability and visual appeal.
This commit is contained in:
2025-04-01 07:55:20 -05:00
parent c9971d5677
commit d201e49409
2 changed files with 634 additions and 0 deletions

368
index.html Normal file
View File

@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
<link rel="stylesheet" href="vendor/paper-css/paper.css">
<link rel="stylesheet" href="styles/styles.css">
<title>Schedule</title>
</head>
<body class="">
<div class="sheet padding-10mm flex">
<header>
<img class="border radius placeholder">
<h1>Schedule</h1>
</header>
<section class="">
</section>
</div>
<script type="text/x-handlebars-template" id="calendar">
{{#*inline "calendar"}}
<table>
<thead>
<tr>
{{#each dotw}}
<th id=""header-month-{{@../../index}}-day-{{@index}}"">{{this}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each weeks}}
<tr data-week="{{@../index}}" class="week">
{{#each ../dotw}}
<td>
<div class="day"
id="month-{{@../../index}}-week-{{@../index}}-day-{{@index}}"
data-month="{{@../../index}}"
data-week="{{@../index}}"
data-day="{{@index}}">
<div class="day-date">
00
</div>
<div class="opponent">
XXX
</div>
<div class="time">
00:00 XX
</div>
<div class="annotation">
</div>
</div></td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
{{/inline}}
{{#each months}}
<div class="grid-item ">
<div class="calendar" data-month="{{@index}}">
<h2>{{this}}</h2>
{{> calendar dotw=../dotw weeks=../weeks}}
</div>
</div>
{{/each}}
</script>
<script>
function repeatBlock(context, options) {
const { count, start, step } = context.hash
const { fn } = context
console.log(context, options)
var max = count * step + start;
var index = start;
var str = '';
do {
var data = {
index,
count,
start,
step,
first: index === start,
last: index >= max - step
};
var blockParams = [index, data];
str += fn(thisArg, { data, blockParams });
index += data.step;
} while (index < max);
return str;
}
</script>
<script>
function range(n) {
const length = Math.max(Math.ceil((end - start) / step), 0);
return Array.from({ length }, (_, i) => start + i * step);
}
function getCalendarWeeks(year, month) {
const weeks = [];
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
let current = new Date(firstDayOfMonth);
current.setDate(current.getDate() - current.getDay()); // Start from the previous Sunday (or same day if Sunday)
while (current <= lastDayOfMonth || current.getDay() !== 0) {
const week = [];
for (let i = 0; i < 7; i++) {
week.push(new Date(current)); // Clone the date
current.setDate(current.getDate() + 1);
}
weeks.push(week);
}
return weeks;
}
function formatDate (date) {
return date.toISOString().split('T')[0]
};
</script>
<script>
function buildMLBScheduleURL({
sportIds = [
1,
// 21, // MiLB (Minor League Baseball)
// 51, // WBC or other international play
],
startDate,
endDate,
gameTypes = [
'E', // Exhibition
'S', // Spring Training
'R', // Regular Season
'F', // Playoffs (Postseason)
'D', // Division Series
'L', // League Championship Series
'W', // World Series
'A', // All-Star
'C' // Wild Card
],
language = 'en',
leagueIds = [
103, // American League
104, // National League
// 590, // Arizona Fall League
// 160, // International League
// 159, // Pacific Coast League
// 420 // Dominican Summer League
],
hydrateFields = [
'team',
'linescore(matchup,runners)',
'xrefId',
'story',
'flags',
'statusFlags',
'broadcasts(all)',
'venue(location)',
'decisions',
'person',
'probablePitcher',
'stats',
'game(content(media(epg),summary))',
'seriesStatus(useOverride=true)'
],
sortBy = [],
teamIds= [],
season
}) {
const baseUrl = 'https://statsapi.mlb.com/api/v1/schedule';
const params = new URLSearchParams();
// Add array-type query params with multiple values
sportIds.forEach(id => params.append('sportId', id));
gameTypes.forEach(type => params.append('gameType', type));
leagueIds.forEach(id => params.append('leagueId', id));
sortBy.forEach(sort => params.append('sortBy', sort));
teamIds.forEach(teamId => params.append('teamId', teamId));
// Add standard query params
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
if (language) params.append('language', language);
if (season) params.append('season', season);
// Add complex hydrate param
if (hydrateFields.length > 0) {
params.append('hydrate', hydrateFields.join(','));
}
return `${baseUrl}?${params.toString()}`;
}
window.addEventListener('load',()=>{
console.log('load')
})
const params = new URLSearchParams(window.location.search);
const YEAR = params.get('year') || 2025 ;
const FAVORITE_TEAM_ID = params.get('teamId') || 112 // 'Chicago Cubs': 112
const START_MONTH = (params.get('startMonth') || 4) - 1 // Apr is month 4, index s/b 0 based
const END_MONTH = (params.get('endMonth') || 9) - 1 // Sep is month 9, index s/b 0 based
const MARK_AB_FOR_120_HOME_GAMES = params.get('mark980') || false
const SHEET_SIZE = params.get("sheetSize") || 'letter'
const header_image = document.querySelector('header img')
header_image.src = `https://www.mlbstatic.com/team-logos/team-cap-on-light/${FAVORITE_TEAM_ID}.svg`
header_image.classList.remove('placeholder', 'border')
document.body.classList.add(SHEET_SIZE)
document.body.classList.add(`team-id-${FAVORITE_TEAM_ID}`)
document.body.querySelector('h1').innerHTML = `${YEAR} Schedule`
const url = buildMLBScheduleURL({
sportIds: [
1,
// 21, // MiLB (Minor League Baseball)
// 51, // WBC or other international play
],
startDate: `${YEAR}-01-01`,
endDate: `${YEAR}-12-31` ,
gameTypes: [
'E', // Exhibition
'S', // Spring Training
'R', // Regular Season
'F', // Playoffs (Postseason)
'D', // Division Series
'L', // League Championship Series
'W', // World Series
'A', // All-Star
'C' // Wild Card
],
language: 'en',
leagueIds: [
103, // American League
104, // National League
// 590, // Arizona Fall League
// 160, // International League
// 159, // Pacific Coast League
// 420 // Dominican Summer League
],
hydrateFields: [
'team',
'broadcasts(all)',
'venue(location)',
],
teamIds: [FAVORITE_TEAM_ID],
season: YEAR
})
// const mlb_response = await fetch(url)
// .then(response=>response.json());
// console.log(mlb_response)
// const response = await fetch(url);
// const mlb_response = await response.json();
// const mlb_response = await fetch(url).then(res=>res.json());
const section = document.querySelector('section')
const table_template = Handlebars.compile(document.querySelector('script#calendar').innerHTML)
Handlebars.registerHelper('json',(s)=>JSON.stringify(s))
Handlebars.registerHelper('repeatBlock', repeatBlock)
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
]
const dotw = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const weeks = [0,1,2,3,4]
const months = monthNames.slice(START_MONTH,END_MONTH+1)
section.innerHTML = table_template({
dotw,
weeks,
months
})
months.forEach((month, month_index) => {
const month_number = monthNames.indexOf(month)
const calendarWeeks = getCalendarWeeks(YEAR, month_number)
calendarWeeks.forEach((week, week_index) => {
week.forEach((day) => {
const day_index = day.getDay(); // 0-6
// console.log(`${day.toDateString()} - Day Index: ${day_index}`);
var cell = document.querySelector(`[data-month="${month_index}"][data-week="${week_index}"][data-day="${day_index}"]`)
if (day.getMonth() == month_number && cell) {
cell.querySelector('.day-date').innerHTML = `${day.getDate()}`
} else if (cell) {
cell.querySelector('.day-date').innerHTML = ""
} else if (day.getMonth() == month_number) {
const prev_div = document.querySelector(`[data-month="${month_index}"][data-week="${week_index-1}"][data-day="${day_index}"]`)
const div = prev_div.cloneNode(true)
// parentElement.classList.add('halved')
// cell.querySelector('.day-date').innerHTML = `${prev}*`
prev_div.classList.add('half')
div.classList.add('half')
div.querySelector('.day-date').innerHTML = `${day.getDate()}`
div.dataset.month = month_index
div.dataset.week = week_index
div.dataset.day = day_index
const td = prev_div.closest('td')
td.append(div)
}
});
});
});
fetch(url)
.then((res)=>res.json())
.then((mlb_response)=>{
months.forEach((month, month_index) => {
const month_number = monthNames.indexOf(month)
const calendarWeeks = getCalendarWeeks(YEAR, month_number)
calendarWeeks.forEach((week, week_index) => {
week.forEach((day) => {
const day_index = day.getDay(); // 0-6
// console.log(`${day.toDateString()} - Day Index: ${day_index}`);
var cell = document.querySelector(`[data-month="${month_index}"][data-week="${week_index}"][data-day="${day_index}"]`)
const games = Array.from(mlb_response.dates).find((row)=>row.date==`${formatDate(day)}`)?.games
if (day.getMonth() == month_number && games && games.length > 0){
games.forEach((game)=>{
const is_home = game.teams.home.team.id == FAVORITE_TEAM_ID ;
const opponent = is_home ? game.teams.away.team : game.teams.home.team
const time_string = (new Date(Date.parse(game.gameDate))).toLocaleTimeString(undefined, {hour:"numeric", minute:"2-digit"})
if (cell){
cell.classList.add('has-game')
if (is_home) {
cell.classList.add('home')
if (MARK_AB_FOR_120_HOME_GAMES && time_string == "1:20 PM" && day_index == 5) {
const top_right = cell.querySelector('.annotation')
const week_is_even = week_index % 2 === 0
top_right.innerHTML = week_is_even ? "B" : "A"
week_is_even ? top_right.classList.add('B') : top_right.classList.add('A')
top_right.classList.add ('friday9_80')
}
} else {
cell.classList.add('away')
}
cell.querySelector('.opponent').innerHTML = `${opponent.abbreviation}`
cell.querySelector('.time').innerHTML = time_string
// cell.querySelector('.time').innerHTML = `${day.getDate()}`
} else {
// Need split day at end of month
}
})
} else if (cell) {
cell.querySelector('.opponent').innerHTML = ""
cell.querySelector('.time').innerHTML = ""
}
});
});
});
})
</script>
</body>
</html>