@@ -65,12 +65,32 @@ async function getAccessToken(
6565 return data . access_token ?? null ;
6666}
6767
68+ function resolveBaseStartDate ( startDateStr ?: string ) : Date {
69+ const tomorrow = new Date ( Date . now ( ) + 86400_000 ) ;
70+ if ( ! startDateStr ) {
71+ return tomorrow ;
72+ }
73+ const parsed = new Date ( `${ startDateStr } T12:00:00Z` ) ;
74+ if ( Number . isNaN ( parsed . getTime ( ) ) ) {
75+ return tomorrow ;
76+ }
77+ const maxFuture = new Date ( Date . now ( ) + 180 * 86400_000 ) ;
78+ if ( parsed < tomorrow ) {
79+ return tomorrow ;
80+ }
81+ if ( parsed > maxFuture ) {
82+ return maxFuture ;
83+ }
84+ return parsed ;
85+ }
86+
6887function generateDefaultCandidateSlots (
69- daysAhead = 14 ,
88+ daysAhead = 21 ,
7089 durationMinutes = 45 ,
90+ startDateStr ?: string ,
7191) : AvailableCalendarSlot [ ] {
7292 const slots : AvailableCalendarSlot [ ] = [ ] ;
73- const now = new Date ( ) ;
93+ const baseDate = resolveBaseStartDate ( startDateStr ) ;
7494
7595 // Candidate slots in overlapping US PT (8:00 AM - 2:00 PM PT) & ET (11:00 AM - 5:00 PM ET) work hours:
7696 // 15:00 UTC (8:00 AM PDT / 11:00 AM EDT), 16:00 UTC (9:00 AM PDT / 12:00 PM EDT),
@@ -84,8 +104,8 @@ function generateDefaultCandidateSlots(
84104 { hour : 20 , minute : 0 , label : "1:00 PM PT / 4:00 PM ET" } ,
85105 ] ;
86106
87- for ( let d = 1 ; d <= daysAhead ; d ++ ) {
88- const dayDate = new Date ( now . getTime ( ) + d * 86400_000 ) ;
107+ for ( let d = 0 ; d < daysAhead ; d ++ ) {
108+ const dayDate = new Date ( baseDate . getTime ( ) + d * 86400_000 ) ;
89109 const dayOfWeek = dayDate . getUTCDay ( ) ;
90110 // Weekdays Monday (1) through Friday (5)
91111 if ( dayOfWeek === 0 || dayOfWeek === 6 ) {
@@ -273,20 +293,60 @@ async function fetchBusyRanges(
273293 }
274294}
275295
296+ function spreadSlotsAcrossDays (
297+ slots : AvailableCalendarSlot [ ] ,
298+ maxSlots : number ,
299+ maxPerDay = 2 ,
300+ ) : AvailableCalendarSlot [ ] {
301+ const selected : AvailableCalendarSlot [ ] = [ ] ;
302+ const perDayCount = new Map < string , number > ( ) ;
303+
304+ for ( const slot of slots ) {
305+ const dayKey = slot . startAt . slice ( 0 , 10 ) ;
306+ const current = perDayCount . get ( dayKey ) ?? 0 ;
307+ if ( current < maxPerDay ) {
308+ selected . push ( slot ) ;
309+ perDayCount . set ( dayKey , current + 1 ) ;
310+ if ( selected . length >= maxSlots ) {
311+ return selected ;
312+ }
313+ }
314+ }
315+
316+ if ( selected . length < maxSlots ) {
317+ for ( const slot of slots ) {
318+ if ( ! selected . some ( ( s ) => s . id === slot . id ) ) {
319+ selected . push ( slot ) ;
320+ if ( selected . length >= maxSlots ) {
321+ break ;
322+ }
323+ }
324+ }
325+ }
326+
327+ return selected . sort (
328+ ( a , b ) => new Date ( a . startAt ) . getTime ( ) - new Date ( b . startAt ) . getTime ( ) ,
329+ ) ;
330+ }
331+
276332/**
277333 * Queries Google Calendar FreeBusy API across Alex Patterson's calendars
278- * (codingcat.dev, flipfeeds.com, alex.patterson@gmail.com, sanity.io, and Oliver family calendars)
279334 * and returns conflict-free slots during 8am-5pm PT/ET work hours.
280335 */
281336export async function getMultiCalendarAvailableSlots (
282337 maxSlots = 9 ,
283338 durationMinutes = 45 ,
339+ startDate ?: string ,
284340) : Promise < {
285341 slots : AvailableCalendarSlot [ ] ;
286342 liveVerified : boolean ;
287343 calendarStatuses : CalendarCheckStatus [ ] ;
288344} > {
289- const candidates = generateDefaultCandidateSlots ( 14 , durationMinutes ) ;
345+ const candidates = generateDefaultCandidateSlots (
346+ 21 ,
347+ durationMinutes ,
348+ startDate ,
349+ ) ;
290350 if ( candidates . length === 0 ) {
291351 return { slots : [ ] , liveVerified : false , calendarStatuses : [ ] } ;
292352 }
@@ -300,7 +360,7 @@ export async function getMultiCalendarAvailableSlots(
300360
301361 if ( ! liveVerified ) {
302362 return {
303- slots : candidates . slice ( 0 , maxSlots ) ,
363+ slots : spreadSlotsAcrossDays ( candidates , maxSlots ) ,
304364 liveVerified : false ,
305365 calendarStatuses,
306366 } ;
@@ -313,7 +373,10 @@ export async function getMultiCalendarAvailableSlots(
313373 } ) ;
314374
315375 return {
316- slots : ( available . length > 0 ? available : candidates ) . slice ( 0 , maxSlots ) ,
376+ slots : spreadSlotsAcrossDays (
377+ available . length > 0 ? available : candidates ,
378+ maxSlots ,
379+ ) ,
317380 liveVerified : true ,
318381 calendarStatuses,
319382 } ;
0 commit comments