-
Notifications
You must be signed in to change notification settings - Fork 50
Maintainer month 2026 update #380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f53912d
f2c37bf
b2237b5
cb57566
1870517
b203079
0aac0aa
473148f
e53d5d3
5c6f252
cbb6476
a9e4d68
96f6413
6718d17
bfd536e
4ca641a
b0f4284
8bd8d47
a0f815f
34d2fd5
d949b4a
0b5ce69
5ebc1e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { createEvents } from 'ics' | ||
|
|
||
| const EVENT_YEAR = 2026 | ||
|
|
||
| const isTBDValue = (value) => | ||
| !value || (typeof value === 'string' && value.toUpperCase() === 'TBD') | ||
|
|
||
| const isAllDayValue = (value) => | ||
| typeof value === 'string' && | ||
| value.toLowerCase().replace(/[\s\-_]/g, '') === 'allday' | ||
|
|
||
| function parseDateParts(dateStr) { | ||
| const [month, day] = dateStr.split('/').map(Number) | ||
| return { month, day } | ||
| } | ||
|
|
||
| function toICSEvent(event) { | ||
| const { month: startMonth, day: startDay } = parseDateParts(event.date) | ||
|
|
||
| const hasTime = | ||
| !isTBDValue(event.UTCStartTime) && !isAllDayValue(event.UTCStartTime) | ||
|
|
||
| const icsEvent = { | ||
| title: event.title, | ||
| description: event.metaDesc || '', | ||
| location: event.location || '', | ||
| url: event.linkUrl || '', | ||
| calName: 'Maintainer Month 2026', | ||
| } | ||
|
|
||
| if (hasTime) { | ||
| const [startHour, startMinute] = event.UTCStartTime.split(':').map(Number) | ||
| icsEvent.start = [EVENT_YEAR, startMonth, startDay, startHour, startMinute] | ||
| icsEvent.startInputType = 'utc' | ||
|
|
||
| if (!isTBDValue(event.UTCEndTime) && !isAllDayValue(event.UTCEndTime)) { | ||
| const [endHour, endMinute] = event.UTCEndTime.split(':').map(Number) | ||
|
|
||
| if (event.endDate && event.endDate !== event.date) { | ||
| const { month: endMonth, day: endDay } = parseDateParts(event.endDate) | ||
| icsEvent.end = [EVENT_YEAR, endMonth, endDay, endHour, endMinute] | ||
| } else { | ||
| icsEvent.end = [EVENT_YEAR, startMonth, startDay, endHour, endMinute] | ||
| } | ||
| icsEvent.startOutputType = 'utc' | ||
| icsEvent.endInputType = 'utc' | ||
| icsEvent.endOutputType = 'utc' | ||
| } else { | ||
| icsEvent.duration = { hours: 1 } | ||
| } | ||
| } else { | ||
| // All-day or TBD: treat as all-day event | ||
| icsEvent.start = [EVENT_YEAR, startMonth, startDay] | ||
| icsEvent.startInputType = 'utc' | ||
|
|
||
| if (event.endDate && event.endDate !== event.date) { | ||
| const { month: endMonth, day: endDay } = parseDateParts(event.endDate) | ||
| // ICS all-day end date is exclusive, so add one day | ||
| const endDate = new Date(Date.UTC(EVENT_YEAR, endMonth - 1, endDay + 1)) | ||
| icsEvent.end = [ | ||
| endDate.getUTCFullYear(), | ||
| endDate.getUTCMonth() + 1, | ||
| endDate.getUTCDate(), | ||
| ] | ||
| } else { | ||
| // Use Date math to handle month boundary rollover | ||
| const endDate = new Date(Date.UTC(EVENT_YEAR, startMonth - 1, startDay + 1)) | ||
| icsEvent.end = [ | ||
| endDate.getUTCFullYear(), | ||
| endDate.getUTCMonth() + 1, | ||
| endDate.getUTCDate(), | ||
| ] | ||
| } | ||
| } | ||
|
|
||
| return icsEvent | ||
| } | ||
|
|
||
| export function generateICS(events) { | ||
| const icsEvents = events.map(toICSEvent) | ||
| const { error, value } = createEvents(icsEvents) | ||
|
|
||
| if (error) { | ||
| throw new Error(`Failed to generate ICS: ${error.message}`) | ||
| } | ||
|
|
||
| return value | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,9 +12,18 @@ dayjs.extend(utc) | |||||
| dayjs.extend(timezone) | ||||||
|
|
||||||
| const TBD = 'TBD' | ||||||
| const ALL_DAY = 'All Day' | ||||||
|
|
||||||
| const isTBDValue = (value) => | ||||||
| !value || (typeof value === 'string' && value.toUpperCase() === 'TBD') | ||||||
|
|
||||||
| const isAllDayValue = (value) => | ||||||
| typeof value === 'string' && | ||||||
| value.toLowerCase().replace(/[\s\-_]/g, '') === 'allday' | ||||||
|
|
||||||
| export const getEvents = (year) => { | ||||||
| const events_path = year ? `content/${year}/events` : 'content/events' | ||||||
| if (!fs.existsSync(events_path)) return [] | ||||||
| const eventFiles = fs.readdirSync(events_path) | ||||||
|
|
||||||
| const events = eventFiles | ||||||
|
|
@@ -108,27 +117,38 @@ const formatEventDateTime = ( | |||||
| formattedEndDate = endUTCDate.format('MMM D') | ||||||
| } | ||||||
|
|
||||||
| // All-day events | ||||||
| if (isAllDayValue(startTime) || isAllDayValue(endTime)) { | ||||||
| return { | ||||||
| date: formattedDate, | ||||||
| startTime: { utc: ALL_DAY, pt: ALL_DAY }, | ||||||
| endTime: { utc: ALL_DAY, pt: ALL_DAY }, | ||||||
| endDate: formattedEndDate, | ||||||
| timeDisplay: 'all-day', | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Start time | ||||||
| let formattedStartTime = { | ||||||
| utc: TBD, | ||||||
| pt: TBD, | ||||||
| } | ||||||
|
|
||||||
| if (startTime) { | ||||||
| let hasSpecificStartTime = false | ||||||
|
|
||||||
| if (!isTBDValue(startTime)) { | ||||||
| const [startHour, startMinute] = startTime.split(':') | ||||||
|
|
||||||
| const UTCStartTime = UTCDate.hour(startHour).minute(startMinute) | ||||||
|
|
||||||
| if (!isNaN(UTCStartTime)) { | ||||||
| const PTStartTime = UTCStartTime.tz('America/Los_Angeles') | ||||||
|
|
||||||
| const formattedUTCStartTime = UTCStartTime.format('HH:mm a') | ||||||
| const formattedPTStartTime = PTStartTime.format('HH:mm a') | ||||||
|
|
||||||
| formattedStartTime = { | ||||||
| utc: formattedUTCStartTime, | ||||||
| pt: formattedPTStartTime, | ||||||
| utc: UTCStartTime.format('h:mm a'), | ||||||
| pt: PTStartTime.format('h:mm a'), | ||||||
| } | ||||||
| hasSpecificStartTime = true | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -138,28 +158,32 @@ const formatEventDateTime = ( | |||||
| pt: TBD, | ||||||
| } | ||||||
|
|
||||||
| if (endTime && endTime !== TBD) { | ||||||
| let hasSpecificEndTime = false | ||||||
|
|
||||||
| if (!isTBDValue(endTime)) { | ||||||
| const [endHour, endMinute] = endTime.split(':') | ||||||
|
|
||||||
| const UTCEndTime = UTCDate.hour(endHour).minute(endMinute) | ||||||
|
|
||||||
| if (!isNaN(UTCEndTime)) { | ||||||
| const PTEndTime = UTCEndTime.tz('America/Los_Angeles') | ||||||
|
|
||||||
| const formattedUTCEndTime = UTCEndTime.format('HH:mm a') | ||||||
| const formattedPTEndTime = PTEndTime.format('HH:mm a') | ||||||
|
|
||||||
| formattedEndTime = { | ||||||
| utc: formattedUTCEndTime, | ||||||
| pt: formattedPTEndTime, | ||||||
| utc: UTCEndTime.format('h:mm a'), | ||||||
| pt: PTEndTime.format('h:mm a'), | ||||||
| } | ||||||
| hasSpecificEndTime = true | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const timeDisplay = | ||||||
| hasSpecificStartTime || hasSpecificEndTime ? 'specific' : 'tbd' | ||||||
|
||||||
| hasSpecificStartTime || hasSpecificEndTime ? 'specific' : 'tbd' | |
| hasSpecificStartTime && hasSpecificEndTime ? 'specific' : 'tbd' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an event has a start time but no end time, this code sets a duration but does not set
startOutputType. That can cause the generated.icsto be interpreted/exported in local time depending on theicslibrary defaults, while the end-time branch forces UTC. Make the time-handling consistent by settingstartOutputType: 'utc'wheneverstartInputTypeis'utc'(including the duration case).