Fixes timeZone() by no longer needing timeZone() (#646)
* Fixes timeZone() by no longer needing timeZone() * Added simple testcase to demonstrate the behaviour of parseZone() vs dayjs() * Fixed eslint errors
This commit is contained in:
		
							parent
							
								
									eb25ef266a
								
							
						
					
					
						commit
						d3fa6cec80
					
				
					 6 changed files with 69 additions and 28 deletions
				
			
		|  | @ -18,8 +18,7 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps | ||||||
|     <DialogPrimitive.Content |     <DialogPrimitive.Content | ||||||
|       {...props} |       {...props} | ||||||
|       className="min-w-[360px] fixed left-1/2 top-1/2 p-6 text-left bg-white rounded shadow-xl overflow-hidden -translate-x-1/2 -translate-y-1/2 sm:align-middle sm:w-full sm:max-w-lg" |       className="min-w-[360px] fixed left-1/2 top-1/2 p-6 text-left bg-white rounded shadow-xl overflow-hidden -translate-x-1/2 -translate-y-1/2 sm:align-middle sm:w-full sm:max-w-lg" | ||||||
|       ref={forwardedRef} |       ref={forwardedRef}> | ||||||
|     > |  | ||||||
|       {children} |       {children} | ||||||
|     </DialogPrimitive.Content> |     </DialogPrimitive.Content> | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import { asStringOrNull } from "@lib/asStringOrNull"; | ||||||
| import { timeZone } from "@lib/clock"; | import { timeZone } from "@lib/clock"; | ||||||
| import useTheme from "@lib/hooks/useTheme"; | import useTheme from "@lib/hooks/useTheme"; | ||||||
| import AvatarGroup from "@components/ui/AvatarGroup"; | import AvatarGroup from "@components/ui/AvatarGroup"; | ||||||
|  | import { parseZone } from "@lib/parseZone"; | ||||||
| 
 | 
 | ||||||
| const BookingPage = (props: any): JSX.Element => { | const BookingPage = (props: any): JSX.Element => { | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|  | @ -183,9 +184,7 @@ const BookingPage = (props: any): JSX.Element => { | ||||||
|                 )} |                 )} | ||||||
|                 <p className="text-green-500 mb-4"> |                 <p className="text-green-500 mb-4"> | ||||||
|                   <CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> |                   <CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> | ||||||
|                   {dayjs(date) |                   {parseZone(date).format(timeFormat + ", dddd DD MMMM YYYY")} | ||||||
|                     .tz(timeZone()) |  | ||||||
|                     .format(timeFormat + ", dddd DD MMMM YYYY")} |  | ||||||
|                 </p> |                 </p> | ||||||
|                 <p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p> |                 <p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p> | ||||||
|               </div> |               </div> | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								lib/parseZone.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/parseZone.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import dayjs from "dayjs"; | ||||||
|  | 
 | ||||||
|  | const ISO8601_OFFSET_FORMAT = /^(.*)([+-])(\d{2}):(\d{2})|(Z)$/; | ||||||
|  | 
 | ||||||
|  | // @see https://github.com/iamkun/dayjs/issues/651#issuecomment-763033265
 | ||||||
|  | // decorates dayjs in order to keep the utcOffset of the given date string
 | ||||||
|  | // ; natively dayjs auto-converts to local time & losing utcOffset info.
 | ||||||
|  | export function parseZone( | ||||||
|  |   date?: dayjs.ConfigType, | ||||||
|  |   format?: dayjs.OptionType, | ||||||
|  |   locale?: string, | ||||||
|  |   strict?: boolean | ||||||
|  | ) { | ||||||
|  |   if (typeof date !== "string") { | ||||||
|  |     return dayjs(date, format, locale, strict); | ||||||
|  |   } | ||||||
|  |   const match = date.match(ISO8601_OFFSET_FORMAT); | ||||||
|  |   if (match === null) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (match[0] === "Z") { | ||||||
|  |     return dayjs( | ||||||
|  |       date, | ||||||
|  |       { | ||||||
|  |         utc: true, | ||||||
|  |         ...format, | ||||||
|  |       }, | ||||||
|  |       locale, | ||||||
|  |       strict | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   const [, dateTime, sign, tzHour, tzMinute] = match; | ||||||
|  |   const uOffset: number = tzHour * 60 + parseInt(tzMinute, 10); | ||||||
|  |   const offset = sign === "+" ? uOffset : -uOffset; | ||||||
|  | 
 | ||||||
|  |   return dayjs( | ||||||
|  |     dateTime, | ||||||
|  |     { | ||||||
|  |       $offset: offset, | ||||||
|  |       ...format, | ||||||
|  |     } as dayjs.OptionType, | ||||||
|  |     locale, | ||||||
|  |     strict | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import prisma from "@lib/prisma"; | import prisma from "@lib/prisma"; | ||||||
| import { getIntegrationName, getIntegrationType } from "@lib/integrations"; | import { getIntegrationName, getIntegrationType } from "@lib/integrations"; | ||||||
| import Shell from "@components/Shell"; | import Shell from "@components/Shell"; | ||||||
| import { useState } from "react"; |  | ||||||
| import { useRouter } from "next/router"; | import { useRouter } from "next/router"; | ||||||
| import { useSession } from "next-auth/client"; | import { useSession } from "next-auth/client"; | ||||||
| import Loader from "@components/Loader"; | import Loader from "@components/Loader"; | ||||||
|  | @ -12,16 +11,10 @@ export default function Integration(props) { | ||||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |   // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|   const [session, loading] = useSession(); |   const [session, loading] = useSession(); | ||||||
| 
 | 
 | ||||||
|   const [showAPIKey, setShowAPIKey] = useState(false); |  | ||||||
| 
 |  | ||||||
|   if (loading) { |   if (loading) { | ||||||
|     return <Loader />; |     return <Loader />; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function toggleShowAPIKey() { |  | ||||||
|     setShowAPIKey(!showAPIKey); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function deleteIntegrationHandler(event) { |   async function deleteIntegrationHandler(event) { | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -170,8 +170,7 @@ export default function Home({ integrations }: Props) { | ||||||
|                     <div className="w-2/12 text-right pt-2"> |                     <div className="w-2/12 text-right pt-2"> | ||||||
|                       <button |                       <button | ||||||
|                         onClick={() => integrationHandler(integration.type)} |                         onClick={() => integrationHandler(integration.type)} | ||||||
|                         className="font-medium text-neutral-900 hover:text-neutral-500" |                         className="font-medium text-neutral-900 hover:text-neutral-500"> | ||||||
|                       > |  | ||||||
|                         Add |                         Add | ||||||
|                       </button> |                       </button> | ||||||
|                     </div> |                     </div> | ||||||
|  | @ -276,8 +275,7 @@ export default function Home({ integrations }: Props) { | ||||||
|     return ( |     return ( | ||||||
|       <Dialog |       <Dialog | ||||||
|         open={isAddCalDavIntegrationDialogOpen} |         open={isAddCalDavIntegrationDialogOpen} | ||||||
|         onOpenChange={(isOpen) => setIsAddCalDavIntegrationDialogOpen(isOpen)} |         onOpenChange={(isOpen) => setIsAddCalDavIntegrationDialogOpen(isOpen)}> | ||||||
|       > |  | ||||||
|         <DialogContent> |         <DialogContent> | ||||||
|           <DialogHeader |           <DialogHeader | ||||||
|             title="Connect to CalDav Server" |             title="Connect to CalDav Server" | ||||||
|  | @ -299,16 +297,14 @@ export default function Home({ integrations }: Props) { | ||||||
|             <Button |             <Button | ||||||
|               type="submit" |               type="submit" | ||||||
|               form={ADD_CALDAV_INTEGRATION_FORM_TITLE} |               form={ADD_CALDAV_INTEGRATION_FORM_TITLE} | ||||||
|               className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900" |               className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> | ||||||
|             > |  | ||||||
|               Save |               Save | ||||||
|             </Button> |             </Button> | ||||||
|             <DialogClose |             <DialogClose | ||||||
|               onClick={() => { |               onClick={() => { | ||||||
|                 setIsAddCalDavIntegrationDialogOpen(false); |                 setIsAddCalDavIntegrationDialogOpen(false); | ||||||
|               }} |               }} | ||||||
|               asChild |               asChild> | ||||||
|             > |  | ||||||
|               <Button color="secondary">Cancel</Button> |               <Button color="secondary">Cancel</Button> | ||||||
|             </DialogClose> |             </DialogClose> | ||||||
|           </div> |           </div> | ||||||
|  | @ -321,8 +317,7 @@ export default function Home({ integrations }: Props) { | ||||||
|     return ( |     return ( | ||||||
|       <Dialog |       <Dialog | ||||||
|         open={isAddAppleIntegrationDialogOpen} |         open={isAddAppleIntegrationDialogOpen} | ||||||
|         onOpenChange={(isOpen) => setIsAddAppleIntegrationDialogOpen(isOpen)} |         onOpenChange={(isOpen) => setIsAddAppleIntegrationDialogOpen(isOpen)}> | ||||||
|       > |  | ||||||
|         <DialogContent> |         <DialogContent> | ||||||
|           <DialogHeader |           <DialogHeader | ||||||
|             title="Connect to Apple Server" |             title="Connect to Apple Server" | ||||||
|  | @ -333,8 +328,7 @@ export default function Home({ integrations }: Props) { | ||||||
|                   className="text-indigo-400" |                   className="text-indigo-400" | ||||||
|                   href="https://appleid.apple.com/account/manage" |                   href="https://appleid.apple.com/account/manage" | ||||||
|                   target="_blank" |                   target="_blank" | ||||||
|                   rel="noopener noreferrer" |                   rel="noopener noreferrer"> | ||||||
|                 > |  | ||||||
|                   https://appleid.apple.com/account/manage
 |                   https://appleid.apple.com/account/manage
 | ||||||
|                 </a> |                 </a> | ||||||
|                 . Your credentials will be stored and encrypted. |                 . Your credentials will be stored and encrypted. | ||||||
|  | @ -357,16 +351,14 @@ export default function Home({ integrations }: Props) { | ||||||
|             <button |             <button | ||||||
|               type="submit" |               type="submit" | ||||||
|               form={ADD_APPLE_INTEGRATION_FORM_TITLE} |               form={ADD_APPLE_INTEGRATION_FORM_TITLE} | ||||||
|               className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900" |               className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> | ||||||
|             > |  | ||||||
|               Save |               Save | ||||||
|             </button> |             </button> | ||||||
|             <DialogClose |             <DialogClose | ||||||
|               onClick={() => { |               onClick={() => { | ||||||
|                 setIsAddAppleIntegrationDialogOpen(false); |                 setIsAddAppleIntegrationDialogOpen(false); | ||||||
|               }} |               }} | ||||||
|               asChild |               asChild> | ||||||
|             > |  | ||||||
|               <Button color="secondary">Cancel</Button> |               <Button color="secondary">Cancel</Button> | ||||||
|             </DialogClose> |             </DialogClose> | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								test/lib/parseZone.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								test/lib/parseZone.test.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import dayjs from "dayjs"; | ||||||
|  | import utc from "dayjs/plugin/utc"; | ||||||
|  | import { parseZone } from "@lib/parseZone"; | ||||||
|  | 
 | ||||||
|  | dayjs.extend(utc); | ||||||
|  | 
 | ||||||
|  | const EXPECTED_DATE_STRING = "2021-06-20T11:59:59+02:00"; | ||||||
|  | 
 | ||||||
|  | it("has the right utcOffset regardless of the local timeZone", async () => { | ||||||
|  |   expect(parseZone(EXPECTED_DATE_STRING).utcOffset()).toEqual(120); | ||||||
|  |   expect(parseZone(EXPECTED_DATE_STRING).format()).toEqual(EXPECTED_DATE_STRING); | ||||||
|  |   expect(dayjs(EXPECTED_DATE_STRING).format()).not.toEqual(EXPECTED_DATE_STRING); | ||||||
|  | }); | ||||||
		Loading…
	
		Reference in a new issue
	
	 Alex van Andel
						Alex van Andel