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
 | 
			
		||||
      {...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"
 | 
			
		||||
      ref={forwardedRef}
 | 
			
		||||
    >
 | 
			
		||||
      ref={forwardedRef}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </DialogPrimitive.Content>
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import { asStringOrNull } from "@lib/asStringOrNull";
 | 
			
		|||
import { timeZone } from "@lib/clock";
 | 
			
		||||
import useTheme from "@lib/hooks/useTheme";
 | 
			
		||||
import AvatarGroup from "@components/ui/AvatarGroup";
 | 
			
		||||
import { parseZone } from "@lib/parseZone";
 | 
			
		||||
 | 
			
		||||
const BookingPage = (props: any): JSX.Element => {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
| 
						 | 
				
			
			@ -183,9 +184,7 @@ const BookingPage = (props: any): JSX.Element => {
 | 
			
		|||
                )}
 | 
			
		||||
                <p className="text-green-500 mb-4">
 | 
			
		||||
                  <CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
 | 
			
		||||
                  {dayjs(date)
 | 
			
		||||
                    .tz(timeZone())
 | 
			
		||||
                    .format(timeFormat + ", dddd DD MMMM YYYY")}
 | 
			
		||||
                  {parseZone(date).format(timeFormat + ", dddd DD MMMM YYYY")}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p>
 | 
			
		||||
              </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 { getIntegrationName, getIntegrationType } from "@lib/integrations";
 | 
			
		||||
import Shell from "@components/Shell";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { useSession } from "next-auth/client";
 | 
			
		||||
import Loader from "@components/Loader";
 | 
			
		||||
| 
						 | 
				
			
			@ -12,16 +11,10 @@ export default function Integration(props) {
 | 
			
		|||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  const [session, loading] = useSession();
 | 
			
		||||
 | 
			
		||||
  const [showAPIKey, setShowAPIKey] = useState(false);
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <Loader />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function toggleShowAPIKey() {
 | 
			
		||||
    setShowAPIKey(!showAPIKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function deleteIntegrationHandler(event) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -170,8 +170,7 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
                    <div className="w-2/12 text-right pt-2">
 | 
			
		||||
                      <button
 | 
			
		||||
                        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
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -276,8 +275,7 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
    return (
 | 
			
		||||
      <Dialog
 | 
			
		||||
        open={isAddCalDavIntegrationDialogOpen}
 | 
			
		||||
        onOpenChange={(isOpen) => setIsAddCalDavIntegrationDialogOpen(isOpen)}
 | 
			
		||||
      >
 | 
			
		||||
        onOpenChange={(isOpen) => setIsAddCalDavIntegrationDialogOpen(isOpen)}>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogHeader
 | 
			
		||||
            title="Connect to CalDav Server"
 | 
			
		||||
| 
						 | 
				
			
			@ -299,16 +297,14 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
            <Button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              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
 | 
			
		||||
            </Button>
 | 
			
		||||
            <DialogClose
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setIsAddCalDavIntegrationDialogOpen(false);
 | 
			
		||||
              }}
 | 
			
		||||
              asChild
 | 
			
		||||
            >
 | 
			
		||||
              asChild>
 | 
			
		||||
              <Button color="secondary">Cancel</Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -321,8 +317,7 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
    return (
 | 
			
		||||
      <Dialog
 | 
			
		||||
        open={isAddAppleIntegrationDialogOpen}
 | 
			
		||||
        onOpenChange={(isOpen) => setIsAddAppleIntegrationDialogOpen(isOpen)}
 | 
			
		||||
      >
 | 
			
		||||
        onOpenChange={(isOpen) => setIsAddAppleIntegrationDialogOpen(isOpen)}>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogHeader
 | 
			
		||||
            title="Connect to Apple Server"
 | 
			
		||||
| 
						 | 
				
			
			@ -333,8 +328,7 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
                  className="text-indigo-400"
 | 
			
		||||
                  href="https://appleid.apple.com/account/manage"
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                  rel="noopener noreferrer"
 | 
			
		||||
                >
 | 
			
		||||
                  rel="noopener noreferrer">
 | 
			
		||||
                  https://appleid.apple.com/account/manage
 | 
			
		||||
                </a>
 | 
			
		||||
                . Your credentials will be stored and encrypted.
 | 
			
		||||
| 
						 | 
				
			
			@ -357,16 +351,14 @@ export default function Home({ integrations }: Props) {
 | 
			
		|||
            <button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              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
 | 
			
		||||
            </button>
 | 
			
		||||
            <DialogClose
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setIsAddAppleIntegrationDialogOpen(false);
 | 
			
		||||
              }}
 | 
			
		||||
              asChild
 | 
			
		||||
            >
 | 
			
		||||
              asChild>
 | 
			
		||||
              <Button color="secondary">Cancel</Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </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