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:
368
index.html
Normal file
368
index.html
Normal 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>
|
||||
266
styles/styles.css
Normal file
266
styles/styles.css
Normal file
@@ -0,0 +1,266 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wdth,wght@0,62.5..100,100..900;1,62.5..100,100..900&display=swap');
|
||||
|
||||
:root {
|
||||
font-family: "Noto Sans", Arial, Helvetica, sans-serif;
|
||||
--black-border: solid black 2px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
&.placeholder{
|
||||
background-color: lightblue;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
border: var(--black-border);
|
||||
}
|
||||
.radius {
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: auto;
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* padding: 0.5em; */
|
||||
}
|
||||
}
|
||||
|
||||
.team-id-112 {
|
||||
th {
|
||||
background-color: navy !important;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
h2 {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
table {
|
||||
/* width:100%; */
|
||||
border-collapse: collapse;
|
||||
th {
|
||||
background-color: black;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: 900;
|
||||
}
|
||||
th, td {
|
||||
border: black solid 1px;
|
||||
width: 65px;
|
||||
&:has(.half){
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:has(.home) {
|
||||
/* background-color: lightblue; */
|
||||
}
|
||||
&:has(.away) {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
td > div {
|
||||
height: auto;
|
||||
aspect-ratio: 10 / 10;
|
||||
align-items: center;
|
||||
& {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
}
|
||||
|
||||
.day-date {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
.opponent {
|
||||
grid-area: 2 / 1 / 3 / 4;
|
||||
}
|
||||
.annotation {
|
||||
margin-right: 2px;
|
||||
grid-area: 1 / 2 / 2 / 4;
|
||||
}
|
||||
.time {
|
||||
grid-area: 3 / 1 / 4 / 4;
|
||||
}
|
||||
|
||||
.friday9_80 {
|
||||
font-size: .7em;
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
&.A{
|
||||
color: gray;
|
||||
text-decoration: underline;
|
||||
}
|
||||
&.B{
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
&.half {
|
||||
height: inherit;
|
||||
aspect-ratio: inherit;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
|
||||
&:first-child{
|
||||
border-bottom: black solid 1px;
|
||||
align-items: top;
|
||||
}
|
||||
|
||||
.day-date {
|
||||
font-size: .9em;
|
||||
margin-left: 1px;
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
.opponent {
|
||||
font-size: .9em;
|
||||
line-height: .9em;
|
||||
grid-area: 2 / 1 / 3 / 3;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.annotation {
|
||||
display:none;
|
||||
}
|
||||
.time {
|
||||
margin-right: 1px;
|
||||
text-wrap: nowrap;
|
||||
grid-area: 1 / 2 / 2 / 3;
|
||||
font-size: .9em;
|
||||
font-stretch: condensed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.opponent {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.day-date{
|
||||
font-weight: 500;
|
||||
font-stretch: semi-condensed;
|
||||
}
|
||||
.time {
|
||||
text-align: center;
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ledger {
|
||||
font-size: 16px;
|
||||
header {
|
||||
padding: 2em;
|
||||
|
||||
img {
|
||||
width: 1.5in;
|
||||
height: 1.5in;
|
||||
}
|
||||
h1 {
|
||||
margin-left: 1em;
|
||||
font-size: xxx-large;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
h2 {
|
||||
line-height: .9em;
|
||||
margin-bottom: .3em;
|
||||
}
|
||||
|
||||
table {
|
||||
th {
|
||||
font-size: .8em;
|
||||
}
|
||||
th, td {
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.opponent {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.month-number{
|
||||
font-size: 1em;
|
||||
}
|
||||
.time {
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.letter, .A4 {
|
||||
header {
|
||||
padding: 1em;
|
||||
|
||||
img {
|
||||
width: .9in;
|
||||
height: .9in;
|
||||
}
|
||||
h1 {
|
||||
margin-left: 1em;
|
||||
font-size: xx-large;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
font-size: 11px;
|
||||
h2 {
|
||||
line-height: .9em;
|
||||
margin-bottom: .3em;
|
||||
}
|
||||
|
||||
table {
|
||||
th {
|
||||
font-size: .8em;
|
||||
}
|
||||
th, td {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
|
||||
.opponent {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.month-number{
|
||||
font-size: 1em;
|
||||
}
|
||||
.time {
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user