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:
co-authored by
Alex van Andel
parent
d88bdc368b
commit
7aa131f349
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user