Completely rebuilt logic when dealing with timezones. Now all available events should appear when selecting a date.
This commit is contained in:
		
							parent
							
								
									96e2b762c6
								
							
						
					
					
						commit
						2b0e8bef7a
					
				
					 3 changed files with 145 additions and 54 deletions
				
			
		
							
								
								
									
										94
									
								
								lib/slots.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								lib/slots.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | |||
| const dayjs = require("dayjs"); | ||||
| 
 | ||||
| const isToday = require("dayjs/plugin/isToday"); | ||||
| const utc = require("dayjs/plugin/utc"); | ||||
| const timezone = require("dayjs/plugin/timezone"); | ||||
| 
 | ||||
| dayjs.extend(isToday); | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| 
 | ||||
| const getMinutesFromMidnight = (date) => { | ||||
|   return date.hour() * 60 + date.minute(); | ||||
| }; | ||||
| 
 | ||||
| const getSlots = ({ | ||||
|   calendarTimeZone, | ||||
|   eventLength, | ||||
|   selectedTimeZone, | ||||
|   selectedDate, | ||||
|   dayStartTime, | ||||
|   dayEndTime | ||||
| }) => { | ||||
| 
 | ||||
|   if(!selectedDate) return [] | ||||
|    | ||||
|   const lowerBound = selectedDate.startOf("day"); | ||||
| 
 | ||||
|   // Simple case, same timezone
 | ||||
|   if (calendarTimeZone === selectedTimeZone) { | ||||
|     const slots = []; | ||||
|     const now = dayjs(); | ||||
|     for ( | ||||
|       let minutes = dayStartTime; | ||||
|       minutes <= dayEndTime - eventLength; | ||||
|       minutes += parseInt(eventLength, 10) | ||||
|     ) { | ||||
|       const slot = lowerBound.add(minutes, "minutes"); | ||||
|       if (slot > now) { | ||||
|         slots.push(slot); | ||||
|       } | ||||
|     } | ||||
|     return slots; | ||||
|   } | ||||
| 
 | ||||
|   const upperBound = selectedDate.endOf("day"); | ||||
| 
 | ||||
|   // We need to start generating slots from the start of the calendarTimeZone day
 | ||||
|   const startDateTime = lowerBound | ||||
|     .tz(calendarTimeZone) | ||||
|     .startOf("day") | ||||
|     .add(dayStartTime, "minutes"); | ||||
| 
 | ||||
|   let phase = 0; | ||||
|   if (startDateTime < lowerBound) { | ||||
|     // Getting minutes of the first event in the day of the chooser
 | ||||
|     const diff = lowerBound.diff(startDateTime, "minutes"); | ||||
| 
 | ||||
|     // finding first event
 | ||||
|     phase = diff + eventLength - (diff % eventLength); | ||||
|   } | ||||
| 
 | ||||
|   // We can stop as soon as the selectedTimeZone day ends
 | ||||
|   const endDateTime = upperBound | ||||
|     .tz(calendarTimeZone) | ||||
|     .subtract(eventLength, "minutes"); | ||||
| 
 | ||||
|   const maxMinutes = endDateTime.diff(startDateTime, "minutes"); | ||||
| 
 | ||||
|   const slots = []; | ||||
|   const now = dayjs(); | ||||
|   for ( | ||||
|     let minutes = phase; | ||||
|     minutes <= maxMinutes; | ||||
|     minutes += parseInt(eventLength, 10) | ||||
|   ) { | ||||
|     const slot = startDateTime.add(minutes, "minutes"); | ||||
| 
 | ||||
|     const minutesFromMidnight = getMinutesFromMidnight(slot); | ||||
| 
 | ||||
|     if ( | ||||
|       minutesFromMidnight < dayStartTime || | ||||
|       minutesFromMidnight > dayEndTime - eventLength || | ||||
|       slot < now | ||||
|     ) { | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     slots.push(slot.tz(selectedTimeZone)); | ||||
|   } | ||||
| 
 | ||||
|   return slots; | ||||
| }; | ||||
| 
 | ||||
| export default getSlots | ||||
|  | @ -1,4 +1,4 @@ | |||
| import {useEffect, useState} from 'react'; | ||||
| import {useEffect, useState, useMemo} from 'react'; | ||||
| import Head from 'next/head'; | ||||
| import Link from 'next/link'; | ||||
| import prisma from '../../lib/prisma'; | ||||
|  | @ -13,6 +13,8 @@ dayjs.extend(isBetween); | |||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| 
 | ||||
| import getSlots from '../../lib/slots' | ||||
| 
 | ||||
| export default function Type(props) { | ||||
|     // Initialise state
 | ||||
|     const [selectedDate, setSelectedDate] = useState(''); | ||||
|  | @ -33,6 +35,21 @@ export default function Type(props) { | |||
|         setSelectedMonth(selectedMonth - 1); | ||||
|     } | ||||
| 
 | ||||
|       // Need to define the bounds of the 24-hour window
 | ||||
|       const lowerBound = useMemo(() => { | ||||
|         if(!selectedDate) { | ||||
|           return  | ||||
|         } | ||||
|    | ||||
|         return selectedDate.startOf('day') | ||||
|       }, [selectedDate]) | ||||
|    | ||||
|       const upperBound = useMemo(() => { | ||||
|         if(!selectedDate) return  | ||||
|          | ||||
|         return selectedDate.endOf('day') | ||||
|       }, [selectedDate]) | ||||
| 
 | ||||
|     // Set up calendar
 | ||||
|     var daysInMonth = dayjs().month(selectedMonth).daysInMonth(); | ||||
|     var days = []; | ||||
|  | @ -41,76 +58,57 @@ export default function Type(props) { | |||
|     } | ||||
| 
 | ||||
|     const calendar = days.map((day) => | ||||
|         <button key={day} onClick={(e) => setSelectedDate(dayjs().month(selectedMonth).date(day).format("YYYY-MM-DD"))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}> | ||||
|         <button key={day} onClick={(e) => setSelectedDate(dayjs().tz(dayjs.tz.guess()).month(selectedMonth).date(day))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}> | ||||
|             {day} | ||||
|         </button> | ||||
|     ); | ||||
| 
 | ||||
|     // Handle date change
 | ||||
|     useEffect(async () => { | ||||
|         if(!selectedDate) { | ||||
|           return | ||||
|         } | ||||
| 
 | ||||
|         setLoading(true); | ||||
|         const res = await fetch('/api/availability/' + user + '?date=' + dayjs(selectedDate).format("YYYY-MM-DD")); | ||||
|         const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`); | ||||
|         const data = await res.json(); | ||||
|         setBusy(data.primary.busy); | ||||
|         setLoading(false); | ||||
|     }, [selectedDate]); | ||||
| 
 | ||||
|     // Set up timeslots
 | ||||
|     let times = []; | ||||
| 
 | ||||
|     // If we're looking at availability throughout the current date, work out the current number of minutes elapsed throughout the day
 | ||||
|     if (selectedDate == dayjs().format("YYYY-MM-DD")) { | ||||
|         var i = (parseInt(dayjs().startOf('hour').format('H') * 60) + parseInt(dayjs().startOf('hour').format('m'))); | ||||
|     } else { | ||||
|         var i = props.user.startTime; | ||||
|     } | ||||
|      | ||||
|     // Adding first availability time
 | ||||
|     times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute')); | ||||
| 
 | ||||
|     // Until day end, push new times every x minutes
 | ||||
|     for (;i < props.user.endTime; i += parseInt(props.eventType.length)) { | ||||
|         times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute').add(props.eventType.length, 'minute')); | ||||
|     } | ||||
|     const times = getSlots({ | ||||
|       calendarTimeZone: props.user.timeZone, | ||||
|       selectedTimeZone: dayjs.tz.guess(), | ||||
|       eventLength: props.eventType.length, | ||||
|       selectedDate: selectedDate, | ||||
|       dayStartTime: props.user.startTime, | ||||
|       dayEndTime: props.user.endTime, | ||||
|     }) | ||||
| 
 | ||||
|     // Check for conflicts
 | ||||
|     for(i = times.length - 1; i >= 0; i -= 1) { | ||||
|         busy.forEach(busyTime => { | ||||
|             let startTime = dayjs(busyTime.start); | ||||
|             let endTime = dayjs(busyTime.end); | ||||
|     for(let i = times.length - 1; i >= 0; i -= 1) { | ||||
|       busy.forEach(busyTime => { | ||||
|           let startTime = dayjs(busyTime.start); | ||||
|           let endTime = dayjs(busyTime.end); | ||||
| 
 | ||||
|             // Check if time has passed
 | ||||
|             if (dayjs(times[i]).isBefore(dayjs())) { | ||||
|                 times.splice(i, 1); | ||||
|             } | ||||
|           // Check if start times are the same
 | ||||
|           if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { | ||||
|               times.splice(i, 1); | ||||
|           } | ||||
| 
 | ||||
|             // Check if start times are the same
 | ||||
|             if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { | ||||
|                 times.splice(i, 1); | ||||
|             } | ||||
| 
 | ||||
|             // Check if time is between start and end times
 | ||||
|             if (dayjs(times[i]).isBetween(startTime, endTime)) { | ||||
|                 times.splice(i, 1); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|       // If event ends after endTime, remove the slot
 | ||||
|       if(dayjs(times[i]).add(props.eventType.length, 'minute') > dayjs(times[i]).hour(0).minute(0).add(props.user.endTime, 'minute') ) { | ||||
|         times.splice(i, 1); | ||||
|       } | ||||
| 
 | ||||
|       //If slot starts before startTime, remove it
 | ||||
|       if(dayjs(times[i]) < dayjs(times[i]).hour(0).minute(0).add(props.user.startTime, 'minute') ) { | ||||
|         times.splice(i, 1); | ||||
|       } | ||||
|           // Check if time is between start and end times
 | ||||
|           if (dayjs(times[i]).isBetween(startTime, endTime)) { | ||||
|               times.splice(i, 1); | ||||
|           } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // Display available times
 | ||||
|     const availableTimes = times.map((time) => | ||||
|         <div key={time.format("YYYY-MM-DDTHH:mm:ss")}> | ||||
|             <Link href={`/${props.user.username}/book?date=${time.format("YYYY-MM-DDTHH:mm:ss")}&type=${props.eventType.id}`}> | ||||
|                 <a key={time.format("YYYY-MM-DDTHH:mm:ss")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a> | ||||
|         <div key={dayjs(time).utc().format()}> | ||||
|             <Link href={`/${props.user.username}/book?date=${dayjs(time).utc().format()}&type=${props.eventType.id}`}> | ||||
|                 <a key={dayjs(time).format("hh:mma")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a> | ||||
|             </Link> | ||||
|         </div> | ||||
|     ); | ||||
|  | @ -205,4 +203,4 @@ export async function getServerSideProps(context) { | |||
|             eventType | ||||
|         }, | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -33,9 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         const calendar = google.calendar({version: 'v3', auth}); | ||||
|         calendar.freebusy.query({ | ||||
|             requestBody: { | ||||
|                 timeMin: req.query.date + "T00:00:00.00Z", | ||||
|                 timeMax: req.query.date + "T23:59:59.59Z", | ||||
|                 timeZone: currentUser.timeZone, | ||||
|                 timeMin: req.query.dateFrom, | ||||
|                 timeMax: req.query.dateTo, | ||||
|                 items: [{ | ||||
|                     "id": "primary" | ||||
|                 }] | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Leonardo Stenico
						Leonardo Stenico