added TimezoneChangeDialog (#4818)

* nit

* added timezoneChangeDialog

* prevent interactOutside

* nit

* fixed title prop clash

* nit

* remind in one day if clicked outside

* Use utcOffset instead of tz string for checking diff

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Peer Richelsen
2022-10-05 14:33:29 +00:00
committed by GitHub
co-authored by Alex van Andel
parent d88bdc368b
commit 7aa131f349
6 changed files with 156 additions and 66 deletions
+1
View File
@@ -542,6 +542,7 @@ const EventTypesPage = () => {
<title>Home | Cal.com</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell
heading={t("event_types_page_title") as string}
subtitle={t("event_types_page_subtitle") as string}
+2 -1
View File
@@ -3,6 +3,7 @@ import type Prisma from "@prisma/client";
import { Prisma as PrismaType, UserPlan } from "@prisma/client";
import { hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
@@ -257,7 +258,7 @@ const createUser = async (
password: await hashPassword(uname),
emailVerified: new Date(),
completedOnboarding: opts?.completedOnboarding ?? true,
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
timeZone: opts?.timeZone ?? dayjs.tz.guess(),
locale: opts?.locale ?? "en",
schedules:
opts?.completedOnboarding ?? true
@@ -1288,5 +1288,12 @@
"fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again</1> or reach out to customer support.",
"calendar_connection_fail": "Calendar connection failed",
"booking_confirmation_success": "Booking confirmation succeeded",
"booking_confirmation_fail": "Booking confirmation failed"
"booking_confirmation_fail": "Booking confirmation failed",
"we_wont_show_again": "We won't show this again",
"couldnt_update_timezone": "We couldn't update the timezone",
"updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}",
"update_timezone": "Update timezone",
"update_timezone_question": "Update Timezone?",
"update_timezone_description": "It seems like your local timezone has changed to {{formattedCurrentTz}}. It's very important to have the correct timezone to prevent bookings at undesired times. Do you want to update it?",
"dont_update": "Don't update"
}
+73
View File
@@ -0,0 +1,73 @@
import { useState, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { DialogContent, showToast } from "@calcom/ui/v2";
import { Dialog } from "@calcom/ui/v2/core/Dialog";
export default function TimezoneChangeDialog() {
const { t } = useLocale();
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
const utils = trpc.useContext();
const userTz = user?.timeZone;
const currentTz = dayjs.tz.guess();
const formattedCurrentTz = currentTz?.replace("_", " ");
// update user settings
const onSuccessMutation = async () => {
showToast(t("updated_timezone_to", { formattedCurrentTz }), "success");
await utils.invalidateQueries(["viewer.me"]);
};
const onErrorMutation = () => {
showToast(t("couldnt_update_timezone"), "error");
};
// update timezone in db
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: onSuccessMutation,
onError: onErrorMutation,
});
function updateTimezone() {
setOpen(false);
mutation.mutate({
timeZone: currentTz,
});
}
// check for difference in user timezone and current browser timezone
const [open, setOpen] = useState(false);
useEffect(() => {
const tzDifferent =
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
setOpen(showDialog);
}, [currentTz, isLoading, userTz]);
// save cookie to not show again
function onCancel(maxAge: number, toast: boolean) {
setOpen(false);
document.cookie = `calcom-timezone-dialog=1;max-age=${maxAge}`;
toast && showToast(t("we_wont_show_again"), "success");
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
title={t("update_timezone_question")}
description={t("update_timezone_description", { formattedCurrentTz })}
type="creation"
actionText={t("update_timezone")}
actionOnClick={() => updateTimezone()}
closeText={t("dont_update")}
onInteractOutside={() => onCancel(86400, false) /* 1 day expire */}
actionOnClose={() => onCancel(7776000, true) /* 3 months expire */}>
{/* todo: save this in db and auto-update when timezone changes (be able to disable??? if yes, /settings)
<Checkbox description="Always update timezone" />
*/}
</DialogContent>
</Dialog>
);
}
+68 -63
View File
@@ -4,6 +4,7 @@ import React, { ReactNode, useState } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "./Button";
@@ -79,73 +80,77 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
};
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
({ children, Icon, actionProps, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
{/*zIndex one less than Toast */}
<DialogPrimitive.Content
{...props}
className={classNames(
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
props.size == "xl"
? "p-8 sm:max-w-[90rem]"
: props.size == "lg"
? "p-8 sm:max-w-[70rem]"
: props.size == "md"
? "p-8 sm:max-w-[40rem]"
: "p-8 sm:max-w-[35rem]",
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
`${props.className || ""}`
)}
ref={forwardedRef}>
{props.type === "creation" && (
<div>
{props.title && <DialogHeader title={props.title} />}
{props.description && <p className="pb-5 text-sm text-gray-500">{props.description}</p>}
<div className="flex flex-col space-y-6">{children}</div>
</div>
)}
{props.type === "confirmation" && (
<div className="flex">
{Icon && (
<div className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<Icon className="h-4 w-4 text-black" />
</div>
)}
({ children, title, Icon, actionProps, ...props }, forwardedRef) => {
const { t } = useLocale();
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
{/*zIndex one less than Toast */}
<DialogPrimitive.Content
{...props}
className={classNames(
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
props.size == "xl"
? "p-8 sm:max-w-[90rem]"
: props.size == "lg"
? "p-8 sm:max-w-[70rem]"
: props.size == "md"
? "p-8 sm:max-w-[40rem]"
: "p-8 sm:max-w-[35rem]",
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
`${props.className || ""}`
)}
ref={forwardedRef}>
{props.type === "creation" && (
<div>
{props.title && <DialogHeader title={props.title} />}
{props.description && <p className="mb-6 text-sm text-gray-500">{props.description}</p>}
{title && <DialogHeader title={title} />}
{props.description && <p className="pb-5 text-sm text-gray-500">{props.description}</p>}
<div className="flex flex-col space-y-6">{children}</div>
</div>
</div>
)}
{!props.useOwnActionButtons && (
<DialogFooter>
<div className="mt-2 flex space-x-2">
<DialogClose asChild>
{/* This will require the i18n string passed in */}
<Button color="minimal" onClick={props.actionOnClose}>
{props.closeText ?? "Close"}
</Button>
</DialogClose>
{props.actionOnClick ? (
<Button
color="primary"
disabled={props.actionDisabled}
onClick={props.actionOnClick}
{...actionProps}>
{props.actionText}
</Button>
) : (
<Button color="primary" type="submit" disabled={props.actionDisabled} {...actionProps}>
{props.actionText}
</Button>
)}
{props.type === "confirmation" && (
<div className="flex">
{Icon && (
<div className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<Icon className="h-4 w-4 text-black" />
</div>
)}
<div>
{title && <DialogHeader title={title} />}
{props.description && <p className="mb-6 text-sm text-gray-500">{props.description}</p>}
</div>
</div>
</DialogFooter>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
)}
{!props.useOwnActionButtons && (
<DialogFooter>
<div className="mt-2 flex space-x-2">
<DialogClose asChild>
{/* This will require the i18n string passed in */}
<Button color="minimal" onClick={props.actionOnClose}>
{props.closeText ?? t("close")}
</Button>
</DialogClose>
{props.actionOnClick ? (
<Button
color="primary"
disabled={props.actionDisabled}
onClick={props.actionOnClick}
{...actionProps}>
{props.actionText}
</Button>
) : (
<Button color="primary" type="submit" disabled={props.actionDisabled} {...actionProps}>
{props.actionText}
</Button>
)}
</div>
</DialogFooter>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}
);
type DialogHeaderProps = {
+4 -1
View File
@@ -10,7 +10,6 @@ import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { JOIN_SLACK, ROADMAP, DESKTOP_APP_LINK, WEBAPP_URL } from "@calcom/lib/constants";
@@ -27,6 +26,7 @@ import Dropdown, {
DropdownMenuPortal,
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import TimezoneChangeDialog from "@calcom/ui/TimezoneChangeDialog";
import Button from "@calcom/ui/v2/core/Button";
/* TODO: Get this from endpoint */
@@ -136,6 +136,9 @@ const Layout = (props: LayoutProps) => {
<Toaster position="bottom-right" />
</div>
{/* todo: only run this if timezone is different */}
<TimezoneChangeDialog />
<div className="flex h-screen overflow-hidden" data-testid="dashboard-shell">
{props.SidebarContainer || <SideBarContainer />}
<div className="flex w-0 flex-1 flex-col overflow-hidden">