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
components
lib
pages/integrations
test/lib
|
@ -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