refactor: next/router hooks with next/navigation hooks (#9105)

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
Omar López 2023-08-02 02:35:48 -07:00 committed by GitHub
parent 4ca9138e01
commit b7851e6e53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 1802 additions and 2565 deletions

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
@ -10,7 +10,15 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText, Avatar } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
type ShouldHighlight =
| {
slug: string;
shouldHighlight: true;
}
| {
shouldHighlight?: never;
slug?: never;
};
type AppListCardProps = {
logo?: string;
@ -47,14 +55,16 @@ export default function AppListCard(props: AppListCardProps) {
const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const searchParams = useSearchParams();
const pathname = usePathname();
useEffect(() => {
if (shouldHighlight && highlight) {
const timer = setTimeout(() => {
setHighlight(false);
const url = new URL(window.location.href);
url.searchParams.delete("hl");
router.replace(url.pathname, undefined, { shallow: true });
const _searchParams = new URLSearchParams(searchParams);
_searchParams.delete("hl");
router.replace(`${pathname}?${_searchParams.toString()}`);
}, 3000);
timeoutRef.current = timer;
}

View File

@ -1,26 +0,0 @@
import { useSession } from "next-auth/react";
import React from "react";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
export default function AppsShell({ children }: { children: React.ReactNode }) {
const { status } = useSession();
return (
<>
<div className="mb-12 block lg:hidden">{status === "authenticated" && <NavTabs tabs={tabs} />}</div>
<main className="pb-6">{children}</main>
</>
);
}

View File

@ -1,31 +0,0 @@
import React from "react";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
export default function BookingsShell({ children }: { children: React.ReactNode }) {
return (
<>
<NavTabs tabs={tabs} />
<main>{children}</main>
</>
);
}

View File

@ -2,8 +2,7 @@ import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { useSession } from "next-auth/react";
import type { TFunction } from "next-i18next";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { MutableRefObject, RefObject } from "react";
import { createRef, forwardRef, useRef, useState } from "react";
import type { ControlProps } from "react-select";
@ -13,7 +12,7 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { AvailableTimes } from "@calcom/features/bookings";
import { useInitializeBookerStore, useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
@ -21,30 +20,29 @@ import DatePicker from "@calcom/features/calendars/DatePicker";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
import { CAL_URL } from "@calcom/lib/constants";
import { APP_NAME, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { APP_NAME, CAL_URL, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { weekdayToWeekIndex } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { TimezoneSelect } from "@calcom/ui";
import {
Button,
ColorPicker,
Dialog,
DialogClose,
DialogFooter,
DialogContent,
DialogFooter,
HorizontalTabs,
Label,
Select,
showToast,
Switch,
TextArea,
TextField,
ColorPicker,
Select,
TimezoneSelect,
} from "@calcom/ui";
import { Code, Trello, Sun, ArrowLeft, ArrowDown, ArrowUp } from "@calcom/ui/components/icon";
import { ArrowDown, ArrowLeft, ArrowUp, Code, Sun, Trello } from "@calcom/ui/components/icon";
type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined;
type EmbedType = "inline" | "floating-popup" | "element-click" | "email";
@ -87,25 +85,31 @@ const getDimension = (dimension: string) => {
return dimension;
};
const goto = (router: NextRouter, searchParams: Record<string, string>) => {
const newQuery = new URLSearchParams(router.asPath.split("?")[1]);
Object.keys(searchParams).forEach((key) => {
newQuery.set(key, searchParams[key]);
});
router.push(`${router.asPath.split("?")[0]}?${newQuery.toString()}`, undefined, {
shallow: true,
});
};
function useRouterHelpers() {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const removeQueryParams = (router: NextRouter, queryParams: string[]) => {
const params = new URLSearchParams(window.location.search);
const goto = (newSearchParams: Record<string, string>) => {
const newQuery = new URLSearchParams(searchParams);
Object.keys(newSearchParams).forEach((key) => {
newQuery.set(key, newSearchParams[key]);
});
router.push(`${pathname}?${newQuery.toString()}`);
};
queryParams.forEach((param) => {
params.delete(param);
});
const removeQueryParams = (queryParams: string[]) => {
const params = new URLSearchParams(searchParams);
router.push(`${router.asPath.split("?")[0]}?${params.toString()}`);
};
queryParams.forEach((param) => {
params.delete(param);
});
router.push(`${pathname}?${params.toString()}`);
};
return { goto, removeQueryParams };
}
const getQueryParam = (queryParam: string) => {
const params = new URLSearchParams(window.location.search);
@ -325,6 +329,8 @@ ${uiInstructionCode}`;
},
};
type EmbedCommonProps = { embedType: EmbedType; calLink: string; previewState: PreviewState };
const getEmbedTypeSpecificString = ({
embedFramework,
embedType,
@ -332,10 +338,7 @@ const getEmbedTypeSpecificString = ({
previewState,
}: {
embedFramework: EmbedFramework;
embedType: EmbedType;
calLink: string;
previewState: PreviewState;
}) => {
} & EmbedCommonProps) => {
const frameworkCodes = Codes[embedFramework];
if (!frameworkCodes) {
throw new Error(`No code available for the framework:${embedFramework}`);
@ -823,77 +826,74 @@ const tabs = [
href: "embedTabName=embed-code",
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
{ embedType: EmbedType; calLink: string; previewState: PreviewState }
>(function EmbedHtml({ embedType, calLink, previewState }, ref) {
const { t } = useLocale();
if (ref instanceof Function || !ref) {
return null;
}
if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) {
return null;
}
return (
<>
<div>
<small className="text-subtle flex py-4">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div>
<TextArea
data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
style={{ resize: "none", overflow: "auto" }}
readOnly
value={
`<!-- Cal ${embedType} embed code begins -->\n` +
(embedType === "inline"
? `<div style="width:${getDimension(previewState.inline.width)};height:${getDimension(
previewState.inline.height
)};overflow:scroll" id="my-cal-inline"></div>\n`
: "") +
`<script type="text/javascript">
Component: forwardRef<HTMLTextAreaElement | HTMLIFrameElement | null, EmbedCommonProps>(
function EmbedHtml({ embedType, calLink, previewState }, ref) {
const { t } = useLocale();
if (ref instanceof Function || !ref) {
return null;
}
if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) {
return null;
}
return (
<>
<div>
<small className="text-subtle flex py-4">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div>
<TextArea
data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
style={{ resize: "none", overflow: "auto" }}
readOnly
value={
`<!-- Cal ${embedType} embed code begins -->\n` +
(embedType === "inline"
? `<div style="width:${getDimension(previewState.inline.width)};height:${getDimension(
previewState.inline.height
)};overflow:scroll" id="my-cal-inline"></div>\n`
: "") +
`<script type="text/javascript">
${getEmbedSnippetString()}
${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previewState })}
</script>
<!-- Cal ${embedType} embed code ends -->`
}
/>
<p className="text-subtle hidden text-sm">{t("need_help_embedding")}</p>
</>
);
}),
}
/>
<p className="text-subtle hidden text-sm">{t("need_help_embedding")}</p>
</>
);
}
),
},
{
name: "React",
href: "embedTabName=embed-react",
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
{ embedType: EmbedType; calLink: string; previewState: PreviewState }
>(function EmbedReact({ embedType, calLink, previewState }, ref) {
const { t } = useLocale();
if (ref instanceof Function || !ref) {
return null;
}
if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) {
return null;
}
return (
<>
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
<TextArea
data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
readOnly
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
Component: forwardRef<HTMLTextAreaElement | HTMLIFrameElement | null, EmbedCommonProps>(
function EmbedReact({ embedType, calLink, previewState }, ref) {
const { t } = useLocale();
if (ref instanceof Function || !ref) {
return null;
}
if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) {
return null;
}
return (
<>
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
<TextArea
data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
readOnly
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
/* If you are using yarn */
// yarn add @calcom/embed-react
@ -902,20 +902,21 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
// npm install @calcom/embed-react
${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, previewState })}
`}
/>
</>
);
}),
/>
</>
);
}
),
},
{
name: "Preview",
href: "embedTabName=embed-preview",
icon: Trello,
type: "iframe",
Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null,
{ calLink: string; embedType: EmbedType; previewState: PreviewState }
>(function Preview({ calLink, embedType }, ref) {
Component: forwardRef<HTMLIFrameElement | HTMLTextAreaElement | null, EmbedCommonProps>(function Preview(
{ calLink, embedType },
ref
) {
if (ref instanceof Function || !ref) {
return null;
}
@ -943,7 +944,16 @@ Cal("init", {origin:"${WEBAPP_URL}"});
`;
}
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
const ThemeSelectControl = ({
children,
...props
}: ControlProps<
{
value: Theme;
label: string;
},
false
>) => {
return (
<components.Control {...props}>
<Sun className="text-subtle mr-2 h-4 w-4" />
@ -954,8 +964,7 @@ const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme;
const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
const { goto } = useRouterHelpers();
return (
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
@ -973,7 +982,7 @@ const ChooseEmbedTypesDialogContent = () => {
key={index}
data-testid={embed.type}
onClick={() => {
goto(router, {
goto({
embedType: embed.type,
});
}}>
@ -1375,8 +1384,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
embedType: EmbedType;
embedUrl: string;
}) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const { t } = useLocale();
const router = useRouter();
const { goto, removeQueryParams } = useRouterHelpers();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dialogContentRef = useRef<HTMLDivElement>(null);
const flags = useFlagMap();
@ -1395,10 +1406,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
);
const s = (href: string) => {
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
const _searchParams = new URLSearchParams(searchParams);
const [a, b] = href.split("=");
searchParams.set(a, b);
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
_searchParams.set(a, b);
return `${pathname?.split("?")[0]}?${_searchParams.toString()}`;
};
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
@ -1429,12 +1440,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
const close = () => {
removeQueryParams(router, ["dialog", ...queryParamsForDialog]);
removeQueryParams(["dialog", ...queryParamsForDialog]);
};
// Use embed-code as default tab
if (!router.query.embedTabName) {
goto(router, {
if (!searchParams?.get("embedTabName")) {
goto({
embedTabName: "embed-code",
});
}
@ -1568,7 +1579,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<button
className="h-6 w-6"
onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]);
removeQueryParams(["embedType", "embedTabName"]);
}}>
<ArrowLeft className="mr-4 w-4" />
</button>
@ -1865,7 +1876,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
key={tab.href}
className={classNames(
router.query.embedTabName === tab.href.split("=")[1]
searchParams?.get("embedTabName") === tab.href.split("=")[1]
? "flex flex-grow flex-col"
: "hidden"
)}>
@ -1886,7 +1897,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
)}
</div>
<div className={router.query.embedTabName == "embed-preview" ? "mt-2 block" : "hidden"} />
<div
className={
searchParams?.get("embedTabName") === "embed-preview" ? "mt-2 block" : "hidden"
}
/>
<DialogFooter className="mt-10 flex-row-reverse gap-x-2" showDivider>
<DialogClose />
{tab.type === "code" ? (
@ -1925,7 +1940,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}
/>
</div>
<div className={router.query.embedTabName == "embed-preview" ? "mt-2 block" : "hidden"} />
<div
className={searchParams?.get("embedTabName") === "embed-preview" ? "mt-2 block" : "hidden"}
/>
<DialogFooter className="mt-10 flex-row-reverse gap-x-2" showDivider>
<DialogClose />
<Button
@ -1945,15 +1962,15 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
};
export const EmbedDialog = () => {
const router = useRouter();
const embedUrl: string = router.query.embedUrl as string;
const searchParams = useSearchParams();
const embedUrl = searchParams?.get("embedUrl") as string;
return (
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
{!router.query.embedType ? (
{!searchParams?.get("embedType") ? (
<ChooseEmbedTypesDialogContent />
) : (
<EmbedTypeCodeAndPreviewDialogContent
embedType={router.query.embedType as EmbedType}
embedType={searchParams?.get("embedType") as EmbedType}
embedUrl={embedUrl}
/>
)}
@ -1976,10 +1993,10 @@ export const EmbedButton = <T extends React.ElementType>({
eventId,
...props
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
const router = useRouter();
const { goto } = useRouterHelpers();
className = classNames("hidden lg:inline-flex", className);
const openEmbedModal = () => {
goto(router, {
goto({
dialog: "embed",
eventId: eventId ? eventId.toString() : "",
embedUrl,

View File

@ -1,99 +0,0 @@
import { noop } from "lodash";
import type { LinkProps } from "next/link";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC, MouseEventHandler } from "react";
import { Fragment } from "react";
import { PermissionContainer } from "@calcom/features/auth/PermissionContainer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames";
import type { SVGComponent } from "@lib/types/SVGComponent";
export interface NavTabProps {
tabs: {
name: string;
/** If you want to change the path as per current tab */
href?: string;
/** If you want to change query param tabName as per current tab */
tabName?: string;
icon?: SVGComponent;
adminRequired?: boolean;
className?: string;
}[];
linkProps?: Omit<LinkProps, "href">;
}
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const router = useRouter();
const { t } = useLocale();
return (
<>
<nav
className="no-scrollbar -mb-px flex space-x-5 overflow-x-scroll rtl:space-x-reverse sm:rtl:space-x-reverse"
aria-label="Tabs"
{...props}>
{tabs.map((tab) => {
if ((tab.tabName && tab.href) || (!tab.tabName && !tab.href)) {
throw new Error("Use either tabName or href");
}
let href = "";
let isCurrent;
if (tab.href) {
href = tab.href;
isCurrent = router.asPath === tab.href;
} else if (tab.tabName) {
href = "";
isCurrent = router.query.tabName === tab.tabName;
}
const onClick: MouseEventHandler = tab.tabName
? (e) => {
e.preventDefault();
router.push({
query: {
...router.query,
tabName: tab.tabName,
},
});
}
: noop;
const Component = tab.adminRequired ? PermissionContainer : Fragment;
const className = tab.className || "";
return (
<Component key={tab.name}>
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
<a
onClick={onClick}
className={classNames(
isCurrent
? "text-emphasis border-gray-900"
: "hover:border-default hover:text-default text-subtle border-transparent",
"group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
className
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{t(tab.name)}</span>
</a>
</Link>
</Component>
);
})}
</nav>
<hr className="border-subtle" />
</>
);
};
export default NavTabs;

View File

@ -1,60 +0,0 @@
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "profile",
href: "/settings/my-account/profile",
icon: User,
},
{
name: "teams",
href: "/settings/teams",
icon: Users,
},
{
name: "security",
href: "/settings/security",
icon: Key,
},
{
name: "developer",
href: "/settings/developer",
icon: Terminal,
},
{
name: "billing",
href: "/settings/billing",
icon: CreditCard,
},
{
name: "admin",
href: "/settings/admin",
icon: Lock,
adminRequired: true,
},
];
export default function SettingsShell({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell {...rest} hideHeadingOnMobile>
<div className="sm:mx-auto">
<NavTabs tabs={tabs} />
</div>
<main className="max-w-4xl">
<>
<ErrorBoundary>{children}</ErrorBoundary>
</>
</main>
</Shell>
);
}

View File

@ -1,392 +1,17 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import Shell from "@calcom/features/shell/Shell";
import classNames from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType, AppFrontendPayload } from "@calcom/types/App";
import type { ButtonProps } from "@calcom/ui";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
Dropdown,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuPortal,
DropdownMenuLabel,
DropdownItem,
Avatar,
} from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
import { HeadSeo } from "@calcom/ui";
/* These app slugs all require Google Cal to be installed */
const Component = ({
name,
type,
logo,
slug,
variant,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
concurrentMeetings,
}: Parameters<typeof App>[0]) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasDescriptionItems && (
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
</div>
)}
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
{" "}
<a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })}
</a>
</h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header>
</div>
{!appDbQuery.isLoading ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
</Button>
{!isGlobal && (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
concurrentMeetings={concurrentMeetings}
multiInstall
{...props}
/>
);
}}
/>
)}
</div>
) : showDisconnectIntegration ? (
<DisconnectIntegration
buttonProps={{ color: "secondary" }}
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
appDbQuery.refetch();
}}
/>
) : (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
credentials={appDbQuery.data?.credentials}
concurrentMeetings={concurrentMeetings}
{...props}
/>
);
}}
/>
))
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</span>
)}
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</span>
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
);
};
import type { AppPageProps } from "./AppPage";
import { AppPage } from "./AppPage";
const ShellHeading = () => {
const { t } = useLocale();
return <span className="block py-2">{t("app_store")}</span>;
};
export default function App(props: {
name: string;
description: AppType["description"];
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
slug: string;
variant: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
concurrentMeetings?: boolean;
}) {
export default function WrappedApp(props: AppPageProps) {
return (
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
<HeadSeo
@ -396,115 +21,11 @@ export default function App(props: {
/>
{props.licenseRequired ? (
<LicenseRequired>
<Component {...props} />
<AppPage {...props} />
</LicenseRequired>
) : (
<Component {...props} />
<AppPage {...props} />
)}
</Shell>
);
}
const InstallAppButtonChild = ({
userAdminTeams,
addAppMutationInput,
appCategories,
multiInstall,
credentials,
concurrentMeetings,
...props
}: {
userAdminTeams?: UserAdminTeams;
addAppMutationInput: { type: AppFrontendPayload["type"]; variant: string; slug: string };
appCategories: string[];
multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
concurrentMeetings?: boolean;
} & ButtonProps) => {
const { t } = useLocale();
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) {
return (
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
);
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
}}>
{mutation.isLoading && (
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<Spinner />
</div>
)}
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{userAdminTeams.map((team) => {
const isInstalled =
credentials &&
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
);
return (
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={isInstalled}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>
{team.name} {isInstalled && `(${t("installed")})`}
</p>
</DropdownItem>
);
})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
};

View File

@ -0,0 +1,367 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
import { InstallAppButtonChild } from "./InstallAppButtonChild";
export type AppPageProps = {
name: string;
description: AppType["description"];
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
slug: string;
variant: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
dependencies?: string[];
concurrentMeetings: AppType["concurrentMeetings"];
};
export const AppPage = ({
name,
type,
logo,
slug,
variant,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
concurrentMeetings,
}: AppPageProps) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
enabled: !!dependencies,
});
const disableInstall =
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
// const disableInstall = requiresGCal && !gCalInstalled.data;
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
// Such apps, can only be installed once.
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasDescriptionItems && (
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
<img
key={descriptionItem}
src={descriptionItem}
alt={`Screenshot of app ${name}`}
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
/>
)
)
) : (
<SkeletonText />
)}
</div>
)}
<div
className={classNames(
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
hasDescriptionItems && "lg:ml-8"
)}>
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
{" "}
<a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })}
</a>
</h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header>
</div>
{!appDbQuery.isLoading ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
</Button>
{!isGlobal && (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
multiInstall
concurrentMeetings={concurrentMeetings}
{...props}
/>
);
}}
/>
)}
</div>
) : showDisconnectIntegration ? (
<DisconnectIntegration
buttonProps={{ color: "secondary" }}
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
appDbQuery.refetch();
}}
/>
) : (
<InstallAppButton
type={type}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
...props,
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
};
}
return (
<InstallAppButtonChild
appCategories={categories}
userAdminTeams={appDbQuery.data?.userAdminTeams}
addAppMutationInput={{ type, variant, slug }}
credentials={appDbQuery.data?.credentials}
concurrentMeetings={concurrentMeetings}
{...props}
/>
);
}}
/>
))
) : (
<SkeletonButton className="h-10 w-24" />
)}
{dependencies &&
(!dependencyData.isLoading ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
) : (
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
<span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</span>
)}
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</span>
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
);
};

View File

@ -0,0 +1,124 @@
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppFrontendPayload } from "@calcom/types/App";
import type { ButtonProps } from "@calcom/ui";
import {
Avatar,
Button,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuTrigger,
showToast,
} from "@calcom/ui";
export const InstallAppButtonChild = ({
userAdminTeams,
addAppMutationInput,
appCategories,
multiInstall,
credentials,
concurrentMeetings,
...props
}: {
userAdminTeams?: UserAdminTeams;
addAppMutationInput: { type: AppFrontendPayload["type"]; variant: string; slug: string };
appCategories: string[];
multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
concurrentMeetings?: boolean;
} & ButtonProps) => {
const { t } = useLocale();
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) {
return (
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
);
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
data-testid="install-app-button"
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
}}>
{mutation.isLoading && (
<div className="z-1 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<Spinner />
</div>
)}
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{userAdminTeams.map((team) => {
const isInstalled =
credentials &&
credentials.some((credential) =>
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
);
return (
<DropdownItem
type="button"
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={isInstalled}
StartIcon={(props) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
);
}}>
<p>
{team.name} {isInstalled && `(${t("installed")})`}
</p>
</DropdownItem>
);
})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
};

View File

@ -1,5 +1,5 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React from "react";

View File

@ -1,5 +1,5 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { usePathname, useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React, { useEffect } from "react";
@ -10,9 +10,9 @@ import { ErrorBoundary } from "@calcom/ui";
export default function AdminLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const pathname = usePathname();
const session = useSession();
const router = useRouter();
@ -23,7 +23,7 @@ export default function AdminLayout({
}
}, [session, router]);
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">

View File

@ -1,228 +0,0 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC } from "react";
import { useEffect, useState, useCallback } from "react";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
type AvailableTimesProps = {
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
eventTypeId: number;
recurringCount: number | undefined;
eventTypeSlug: string;
date?: Dayjs;
seatsPerTimeSlot?: number | null;
bookingAttendees?: number | null;
slots?: Slot[];
isLoading: boolean;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
slots = [],
isLoading,
date,
eventTypeId,
eventTypeSlug,
recurringCount,
timeFormat,
onTimeFormatChange,
seatsPerTimeSlot,
bookingAttendees,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
const [brand, setBrand] = useState("#292929");
useEffect(() => {
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
}, []);
const isMobile = useMediaQuery("(max-width: 768px)");
const ref = useCallback(
(node: HTMLDivElement) => {
if (isMobile) {
node?.scrollIntoView({ behavior: "smooth" });
}
},
[isMobile]
);
const reserveSlot = (slot: Slot) => {
// Prevent double clicking
if (reserveSlotMutation.isLoading || reserveSlotMutation.isSuccess) {
return;
}
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
bookingUid: slot.bookingUid,
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-emphasis font-semibold">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-subtle">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">
<ToggleGroup
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
options={[
{ value: "12", label: t("12_hour_short") },
{ value: "24", label: t("24_hour_short") },
]}
/>
</div>
</div>
<div
ref={ref}
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined | TimeFormat>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
query: {
...router.query,
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
timeFormat,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
},
};
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid as string;
}
// If event already has an attendee add booking id
if (slot.bookingUid) {
bookingUrl.query.bookingUid = slot.bookingUid;
}
let slotFull, notEnoughSeats;
if (slot.attendees && seatsPerTimeSlot) slotFull = slot.attendees >= seatsPerTimeSlot;
if (slot.attendees && bookingAttendees && seatsPerTimeSlot) {
notEnoughSeats = slot.attendees + bookingAttendees > seatsPerTimeSlot;
}
const isHalfFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.5;
const isNearlyFull =
slot.attendees && seatsPerTimeSlot && slot.attendees / seatsPerTimeSlot >= 0.83;
const colorClass = isNearlyFull
? "text-rose-600"
: isHalfFull
? "text-yellow-500"
: "text-emerald-400";
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div
className={classNames(
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
data-testid="time"
data-disabled="true">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{notEnoughSeats ? (
<p className="text-sm">{t("not_enough_seats")}</p>
) : slots ? (
<p className="text-sm">{t("booking_full")}</p>
) : null}
</div>
) : (
<Link
href={bookingUrl}
prefetch={false}
className={classNames(
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time"
data-disabled="false">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
<p className={`${colorClass} text-sm`}>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
{seatsPerTimeSlot}{" "}
{t("seats_available", {
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
})}
</p>
)}
</Link>
)}
</div>
);
})}
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
</div>
)}
{isLoading && !slots.length && (
<>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
</>
)}
</div>
</div>
) : null}
</div>
);
};
AvailableTimes.displayName = "AvailableTimes";
export default AvailableTimes;

View File

@ -1,38 +0,0 @@
import { i18n } from "next-i18next";
import type { TFunction } from "next-i18next";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { CreditCard } from "@calcom/ui/components/icon";
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
t: TFunction;
i18n: typeof i18n;
}) => {
const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null;
const params = {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
};
return (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>{props.t("no_show_fee_amount", params)}</>
) : (
<>
{/* If undefined this will default to the browser locale */}
{new Intl.NumberFormat(i18n?.language, {
style: "currency",
currency: paymentAppData.currency,
}).format(paymentAppData.price / 100)}
</>
)}
</p>
);
};
export default BookingDescriptionPayment;

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import type { EventLocationType } from "@calcom/app-store/locations";
@ -158,7 +158,7 @@ function BookingListItem(booking: BookingItemProps) {
id: "cancel",
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
cancel all remaining bookings or just that booking instance. */
href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
@ -239,7 +239,12 @@ function BookingListItem(booking: BookingItemProps) {
},
});
const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => {
const saveLocation = (
newLocationType: EventLocationType["type"],
details: {
[key: string]: string;
}
) => {
let newLocation = newLocationType as string;
const eventLocationType = getEventLocationType(newLocationType);
if (eventLocationType?.organizerInputType) {
@ -255,13 +260,11 @@ function BookingListItem(booking: BookingItemProps) {
.sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
const onClickTableData = () => {
router.push({
pathname: `/booking/${booking.uid}`,
query: {
allRemainingBookings: isTabRecurring,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
},
const urlSearchParams = new URLSearchParams({
allRemainingBookings: isTabRecurring.toString(),
});
if (booking.attendees[0]) urlSearchParams.set("email", booking.attendees[0].email);
router.push(`/booking/${booking.uid}?${urlSearchParams.toString()}`);
};
const title = booking.title;

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -26,6 +26,9 @@ type Props = {
};
export default function CancelBooking(props: Props) {
const pathname = usePathname();
const searchParams = useSearchParams();
const asPath = `${pathname}?${searchParams.toString()}`;
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
@ -97,7 +100,7 @@ export default function CancelBooking(props: Props) {
});
if (res.status >= 200 && res.status < 300) {
await router.replace(router.asPath);
router.replace(asPath);
} else {
setLoading(false);
setError(

View File

@ -1,198 +0,0 @@
import type { EventType } from "@prisma/client";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import type { z } from "zod";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import DatePicker from "@calcom/features/calendars/DatePicker";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useRouterQuery from "@lib/hooks/useRouterQuery";
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
const getRefetchInterval = (refetchCount: number): number => {
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
return intervals[refetchCount] || intervals[intervals.length - 1];
};
const useSlots = ({
eventTypeId,
eventTypeSlug,
startTime,
endTime,
usernameList,
timeZone,
duration,
enabled = true,
}: {
eventTypeId: number;
eventTypeSlug: string;
startTime?: Dayjs;
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
enabled?: boolean;
}) => {
const [refetchCount, setRefetchCount] = useState(0);
const refetchInterval = getRefetchInterval(refetchCount);
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime && enabled,
refetchInterval,
trpc: { context: { skipBatch: true } },
}
);
useEffect(() => {
if (!!data && fetchStatus === "idle") {
setRefetchCount(refetchCount + 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchStatus, data]);
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
};
export const SlotPicker = ({
eventType,
timeFormat,
onTimeFormatChange,
timeZone,
recurringEventCount,
users,
seatsPerTimeSlot,
bookingAttendees,
weekStart = 0,
}: {
eventType: Pick<
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
"id" | "schedulingType" | "slug" | "length" | "metadata"
>;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
bookingAttendees?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
let { duration = eventType.length.toString() } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
if (!eventType.metadata?.multipleDuration) {
duration = eventType.length.toString();
}
useEffect(() => {
if (!router.isReady) return;
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
if (timeZone === "Etc/GMT") {
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
if (date) {
setSelectedDate(dayjs.utc(date));
}
} else {
// Set the start of the month without shifting time like startOf() may do.
setBrowsingDate(
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
);
if (date) {
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: monthSlots, isLoading } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: dayjs().startOf("day"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
/** Prevent refetching is we already have this data from month slots */
enabled: !!selectedDate,
});
/** Hide skeleton if we have the slot loaded in the month query */
const isLoadingSelectedDateSlots = (() => {
if (!selectedDate) return _isLoadingSelectedDateSlots;
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
return false;
})();
return (
<>
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(newDate) => {
setDate(newDate.format("YYYY-MM-DD"));
}}
onMonthChange={(newMonth) => {
setMonth(newMonth.format("YYYY-MM"));
}}
browsingDate={browsingDate}
weekStart={weekStart}
/>
<AvailableTimes
isLoading={isLoadingSelectedDateSlots}
slots={
selectedDate &&
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
monthSlots[selectedDate.format("YYYY-MM-DD")])
}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
duration={parseInt(duration)}
/>
</>
);
};

View File

@ -1,41 +0,0 @@
import type { FC } from "react";
import { useEffect, useState } from "react";
import type { ITimezoneOption } from "@calcom/ui";
import { TimezoneSelect } from "@calcom/ui";
import { timeZone } from "../../lib/clock";
type Props = {
onSelectTimeZone: (selectedTimeZone: string) => void;
};
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
useEffect(() => {
setSelectedTimeZone(timeZone());
}, []);
useEffect(() => {
if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
onSelectTimeZone(timeZone(selectedTimeZone));
}
}, [selectedTimeZone, onSelectTimeZone]);
return !!selectedTimeZone ? (
<TimezoneSelect
id="timeZone"
classNames={{
singleValue: () => "text-default",
dropdownIndicator: () => "text-default",
menu: () => "!w-64 max-w-[90vw] shadow-dropdown bg-default border-subtle border rounded-md mt-1",
}}
variant="minimal"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
/>
) : null;
};
export default TimeOptions;

View File

@ -1,26 +0,0 @@
import { Globe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import TimeOptions from "@components/booking/TimeOptions";
export function TimezoneDropdown({
onChangeTimeZone,
}: {
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
}) {
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
};
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
);
}

View File

@ -1,44 +0,0 @@
import { CAL_URL } from "@calcom/lib/constants";
import type { AvatarGroupProps } from "@calcom/ui";
import { AvatarGroup } from "@calcom/ui";
export const UserAvatars = ({
profile,
users,
...props
}: {
profile: { image: string | null; name?: string | null; username?: string | null };
showMembers: boolean;
users: { username: string | null; name?: string | null }[];
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
return (
<AvatarGroup
items={
[
{
image: profile.image,
alt: profile.name,
title: profile.name,
href: profile.username ? `${CAL_URL}/${profile.username}` : undefined,
},
...(showMembers
? users.map((user) => ({
title: user.name,
image: `${CAL_URL}/${user.username}/avatar.png`,
alt: user.name || undefined,
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
}))
: []),
].filter((item) => !!item.image) as {
image: string;
alt?: string;
title?: string;
href?: string;
}[]
}
size="sm"
truncateAfter={props.truncateAfter}
/>
);
};

View File

@ -25,6 +25,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
CheckboxField,
Label,
@ -32,9 +33,8 @@ import {
showToast,
TextField,
Tooltip,
Alert,
} from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import { Copy, Edit } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";

View File

@ -1,7 +1,7 @@
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
@ -148,7 +148,7 @@ function EventTypeSingleLayout({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
await router.push("/event-types");
router.push("/event-types");
setDeleteDialogOpen(false);
},
onError: (err) => {

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { Schedule } from "@calcom/features/schedules";
@ -21,12 +20,10 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const { t } = useLocale();
const { nextStep } = props;
const router = useRouter();
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId! },
{
enabled: router.isReady && !!defaultScheduleId,
enabled: !!defaultScheduleId,
}
);

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";

View File

@ -1,25 +1,24 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui";
type TeamType = NonNullable<TeamWithMembers>;
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];
type MemberType = MembersType[number] & { safeBio: string | null };
type TeamTypeWithSafeHtml = Omit<TeamType, "members" | "inviteToken"> & { members: MemberType[] };
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
const routerQuery = useRouterQuery();
const { t } = useLocale();
const router = useRouter();
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
// We don't want to forward orgSlug and user which are route params to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
return (
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>

View File

@ -1,7 +1,7 @@
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { RefCallback } from "react";
import { useEffect, useMemo, useState } from "react";
@ -46,6 +46,9 @@ const obtainNewUsernameChangeCondition = ({
};
const PremiumTextfield = (props: ICustomUsernameProps) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const { t } = useLocale();
const { data: session, update } = useSession();
const {
@ -61,8 +64,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const [user] = trpc.viewer.me.useSuspenseQuery();
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
const [markAsError, setMarkAsError] = useState(false);
const router = useRouter();
const { paymentStatus: recentAttemptPaymentStatus } = router.query;
const recentAttemptPaymentStatus = searchParams?.get("recentAttemptPaymentStatus");
const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const isCurrentUsernamePremium =
@ -120,7 +122,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const paymentLink = `/api/integrations/stripepayment/subscription?intentUsername=${
inputUsernameValue || usernameFromStripe
}&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${router.asPath}`;
}&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${pathname}`;
const ActionButtons = () => {
if (paymentRequired) {
@ -223,7 +225,11 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
onChange={(event) => {
event.preventDefault();
// Reset payment status
delete router.query.paymentStatus;
const _searchParams = new URLSearchParams(searchParams);
_searchParams.delete("paymentStatus");
if (searchParams.toString() !== _searchParams.toString()) {
router.replace(`${pathname}?${_searchParams.toString()}`);
}
setInputUsernameValue(event.target.value);
}}
data-testid="username-input"

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
@ -35,12 +35,12 @@ export const UsernameAvailabilityField = ({
onSuccessMutation,
onErrorMutation,
}: UsernameAvailabilityFieldProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [user] = trpc.viewer.me.useSuspenseQuery();
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
const { username: currentUsername, setQuery: setCurrentUsername } =
router.query["username"] && user.username === null
searchParams?.get("username") && user.username === null
? { username: usernameFromQuery, setQuery: setUsernameFromQuery }
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
const formMethods = useForm({

View File

@ -6,8 +6,7 @@ import type { SSRConfig } from "next-i18next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { ParsedUrlQuery } from "querystring";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
@ -23,20 +22,26 @@ import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
({ children }) => <>{children}</>
);
const I18nextAdapter = appWithTranslation<
NextJsAppProps<SSRConfig> & {
children: React.ReactNode;
}
>(({ children }) => <>{children}</>);
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
NextAppProps<
WithNonceProps & {
themeBasis?: string;
} & Record<string, unknown>
>,
"Component"
> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean;
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
getLayout?: (page: React.ReactElement, router: NextAppProps["router"]) => ReactNode;
PageWrapper?: (props: AppProps) => JSX.Element;
};
@ -48,7 +53,7 @@ type AppPropsWithChildren = AppProps & {
children: ReactNode;
};
const getEmbedNamespace = (query: ReturnType<typeof useRouter>["query"]) => {
const getEmbedNamespace = (query: ParsedUrlQuery) => {
// Mostly embed query param should be available on server. Use that there.
// Use the most reliable detection on client
return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
@ -89,20 +94,19 @@ const enum ThemeSupport {
}
type CalcomThemeProps = PropsWithChildren<
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps, "router"> &
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
>;
const CalcomThemeProvider = (props: CalcomThemeProps) => {
const router = useRouter();
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = getEmbedNamespace(router.query);
const embedNamespace = getEmbedNamespace(props.router.query);
const isEmbedMode = typeof embedNamespace === "string";
return (
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace })}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
@ -148,16 +152,14 @@ function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
router,
}: {
props: Omit<CalcomThemeProps, "children">;
isEmbedMode: boolean;
embedNamespace: string | null;
router: NextRouter;
}) {
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
return props.isBookingPage({ router: props.router });
}
return props.isBookingPage;
})();
@ -263,7 +265,8 @@ const AppProviders = (props: AppPropsWithChildren) => {
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}>
isBookingPage={props.Component.isBookingPage}
router={props.router}>
<FeatureFlagsProvider>
<OrgBrandProvider>
<MetaProvider>{props.children}</MetaProvider>

View File

@ -1,9 +1,9 @@
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
export default function usePublicPage() {
const router = useRouter();
const pathname = usePathname();
const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
router.pathname.startsWith(route)
pathname?.startsWith(route)
);
return isPublicPage;
}

View File

@ -1,36 +1,17 @@
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function useRouterQuery<T extends string>(name: T) {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query: Record<string, string | string[]> = {};
// Following error is thrown by Typescript:
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
// We should change the target to higher ES2019 atleast maybe
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
for (const [key, value] of urlParams) {
if (!query[key]) {
query[key] = value;
} else {
let queryValue = query[key];
if (queryValue instanceof Array) {
queryValue.push(value);
} else {
queryValue = query[key] = [queryValue];
queryValue.push(value);
}
}
}
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set(name, newValue as string);
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return { [name]: query[name], setQuery } as {
return { [name]: searchParams.get(name), setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}

View File

@ -1,31 +1,9 @@
import { useRouter } from "next/router";
import { useMemo } from "react";
import { useSearchParams } from "next/navigation";
export function useToggleQuery(name: string) {
const router = useRouter();
const hrefOff = useMemo(() => {
const query = {
...router.query,
};
delete query[name];
return {
query,
};
}, [router.query, name]);
const hrefOn = useMemo(() => {
const query = {
...router.query,
[name]: "1",
};
return {
query,
};
}, [router.query, name]);
const searchParams = useSearchParams();
return {
hrefOn,
hrefOff,
isOn: router.query[name] === "1",
isOn: searchParams?.get(name) === "1",
};
}

View File

@ -58,7 +58,6 @@
"@radix-ui/react-tooltip": "^1.0.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanem/react-nprogress": "^5.0.44",
"@tanstack/react-query": "^4.3.9",
"@tremor/react": "^2.0.0",
"@types/turndown": "^5.0.1",

View File

@ -1,10 +1,10 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { DOCS_URL, JOIN_DISCORD, WEBSITE_URL, IS_CALCOM } from "@calcom/lib/constants";
import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { BookOpen, Check, ChevronRight, FileText, Shield } from "@calcom/ui/components/icon";
@ -22,9 +22,8 @@ enum pageType {
}
export default function Custom404() {
const pathname = usePathname();
const { t } = useLocale();
const router = useRouter();
const [username, setUsername] = useState<string>("");
const [currentPageType, setCurrentPageType] = useState<pageType>(pageType.USER);
@ -52,7 +51,7 @@ export default function Custom404() {
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
useEffect(() => {
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
const [routerUsername] = router.asPath.replace("%20", "-").split(/[?#]/);
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/);
if (!isValidOrgDomain || !currentOrgDomain) {
const splitPath = routerUsername.split("/");
if (splitPath[1] === "team" && splitPath.length === 3) {
@ -78,14 +77,14 @@ export default function Custom404() {
}
}, []);
const isSuccessPage = router.asPath.startsWith("/booking");
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup");
const isSuccessPage = pathname?.startsWith("/booking");
const isSubpage = pathname?.includes("/", 2) || isSuccessPage;
const isSignup = pathname?.startsWith("/signup");
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
**/
const isInsights = router.asPath.startsWith("/insights");
const isInsights = pathname?.startsWith("/insights");
if (isInsights) {
return (
<>

View File

@ -1,5 +1,5 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -9,8 +9,8 @@ import { Copy } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
export default function Error500() {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
return (
<div className="bg-subtle flex h-screen">
@ -25,20 +25,20 @@ export default function Error500() {
Something went wrong on our end. Get in touch with our support team, and well get it fixed right
away for you.
</p>
{router.query.error && (
{searchParams?.get("error") && (
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
Please provide the following text when contacting support to better help you:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{router.query.error}
{searchParams?.get("error")}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={Copy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
navigator.clipboard.writeText(searchParams?.get("error") as string);
showToast("Link copied!", "success");
}}>
{t("copy")}

View File

@ -2,7 +2,6 @@ import type { DehydratedState } from "@tanstack/react-query";
import classNames from "classnames";
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { Toaster } from "react-hot-toast";
import type { z } from "zod";
@ -18,6 +17,7 @@ import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/featur
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
@ -39,7 +39,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
const { t } = useLocale();
const router = useRouter();
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
@ -47,9 +46,13 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
delete query.orgSlug;
const {
// So it doesn't display in the Link (and make tests fail)
user: _user,
orgSlug: _orgSlug,
...query
} = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/*
const telemetry = useTelemetry();

View File

@ -1,10 +1,10 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
@ -59,8 +59,8 @@ function getRoute(appName: string, pages: string[]) {
const AppPage: AppPageType["default"] = function AppPage(props) {
const appName = props.appName;
const router = useRouter();
const pages = router.query.pages as string[];
const params = useParamsWithFallback();
const pages = (params.pages || []) as string[];
const route = getRoute(appName, pages);
const componentProps = {

View File

@ -1,6 +1,6 @@
import type { GetStaticPaths, InferGetStaticPropsType } from "next";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
@ -9,8 +9,9 @@ import { HeadSeo } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
const searchParams = useSearchParams();
const router = useRouter();
const slug = router.query.slug as string;
const slug = searchParams?.get("slug") as string;
const { status } = useSession();
if (status === "loading") {
@ -18,12 +19,10 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
}
if (status === "unauthenticated") {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `/apps/${slug}/setup`,
},
const urlSearchParams = new URLSearchParams({
callbackUrl: `/apps/${slug}/setup`,
});
router.replace(`/auth/login?${urlSearchParams.toString()}`);
}
return (

View File

@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
@ -13,9 +13,9 @@ import { AppCard, SkeletonText } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
const searchParams = useSearchParams();
const { t, isLocaleReady } = useLocale();
const router = useRouter();
const { category } = router.query;
const category = searchParams?.get("category");
return (
<>

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useCallback, useReducer, useState } from "react";
import z from "zod";
@ -350,10 +350,9 @@ type ModalState = {
};
export default function InstalledApps() {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const category = router.query.category as querySchemaType["category"];
const category = searchParams?.get("category") as querySchemaType["category"];
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
// Exclude calendar and other from categoryList, we handle those slightly differently below
return !(category in { other: null, calendar: null });

View File

@ -1,11 +1,10 @@
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import type { ReactElement } from "react";
import { useSearchParams } from "next/navigation";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, SkeletonText } from "@calcom/ui";
import { Button } from "@calcom/ui";
import { X } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@ -19,13 +18,10 @@ const querySchema = z.object({
export default function Error() {
const { t } = useLocale();
const router = useRouter();
const { error } = querySchema.parse(router.query);
const searchParams = useSearchParams();
const { error } = querySchema.parse(searchParams);
const isTokenVerificationError = error?.toLowerCase() === "verification";
let errorMsg: string | ReactElement = <SkeletonText />;
if (router.isReady) {
errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
}
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
return (
<AuthContainer title="" description="">

View File

@ -3,7 +3,7 @@ import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import type { CSSProperties, SyntheticEvent } from "react";
import React from "react";

View File

@ -4,7 +4,7 @@ import { jwtVerify } from "jose";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import type { CSSProperties } from "react";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
@ -49,6 +49,7 @@ export default function Login({
samlProductID,
totpEmail,
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const formSchema = z
@ -77,7 +78,7 @@ export default function Login({
const telemetry = useTelemetry();
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
let callbackUrl = searchParams.get("callbackUrl") || "";
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
@ -169,7 +170,7 @@ export default function Login({
<EmailField
id="email"
label={t("email_address")}
defaultValue={totpEmail || (router.query.email as string)}
defaultValue={totpEmail || (searchParams?.get("email") as string)}
placeholder="john.doe@example.com"
required
{...register("email")}

View File

@ -1,6 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";

View File

@ -1,19 +1,15 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import PageWrapper from "@components/PageWrapper";
// To handle the IdP initiated login flow callback
export default function Page() {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
if (!router.isReady) {
return;
}
const { code } = router.query;
const code = searchParams?.get("code");
signIn("saml-idp", {
callbackUrl: "/",

View File

@ -1,5 +1,5 @@
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
@ -19,15 +19,25 @@ import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { ssrInit } from "@server/lib/ssr";
function useSetStep() {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set("step", newStep.toString());
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return setStep;
}
export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = (newStep: number) => {
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
};
const setStep = useSetStep();
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{

View File

@ -1,6 +1,6 @@
import type { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
@ -27,11 +27,12 @@ import { ssrInit } from "@server/lib/ssr";
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Provider(props: SSOProviderPageProps) {
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
if (props.provider === "saml") {
const email = typeof router.query?.email === "string" ? router.query?.email : null;
const email = searchParams?.get("email");
if (!email) {
router.push("/auth/error?error=" + "Email not provided");

View File

@ -1,5 +1,5 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";

View File

@ -1,6 +1,6 @@
import { MailOpenIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { APP_NAME } from "@calcom/lib/constants";

View File

@ -1,14 +1,14 @@
import { signIn } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import * as React from "react";
import { useEffect, useState, useRef } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
import { AlertTriangle, Check, MailOpen } from "@calcom/ui/components/icon";
import Loader from "@components/Loader";
import PageWrapper from "@components/PageWrapper";
@ -54,8 +54,11 @@ const querySchema = z.object({
});
export default function Verify() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const { t, sessionId, stripeCustomerId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { t, sessionId, stripeCustomerId } = querySchema.parse(routerQuery);
const [secondsLeft, setSecondsLeft] = useState(30);
const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery({
stripeCustomerId,
@ -88,7 +91,7 @@ export default function Verify() {
}
}, [secondsLeft]);
if (!router.isReady || !data) {
if (!data) {
// Loading state
return <Loader />;
}
@ -106,7 +109,7 @@ export default function Verify() {
<Head>
<title>
{/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
it or too hard to read. */}
it or too hard to read. */}
{hasPaymentFailed
? "Your payment failed"
: sessionId
@ -155,16 +158,9 @@ export default function Verify() {
e.preventDefault();
setSecondsLeft(30);
// Update query params with t:timestamp, shallow: true doesn't re-render the page
router.push(
router.asPath,
{
query: {
...router.query,
t: Date.now(),
},
},
{ shallow: true }
);
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set("t", `${Date.now()}`);
router.replace(`${pathname}?${_searchParams.toString()}`);
return await sendVerificationLogin(customer.email, customer.username);
}}>
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}

View File

@ -1,7 +1,6 @@
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
@ -9,13 +8,20 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
ConfirmationDialogContent,
Dialog,
DialogTrigger,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Label,
showToast,
@ -24,26 +30,14 @@ import {
Switch,
TimezoneSelect,
Tooltip,
Dialog,
DialogTrigger,
DropdownMenuSeparator,
Dropdown,
DropdownMenuContent,
DropdownItem,
DropdownMenuTrigger,
ConfirmationDialogContent,
VerticalDivider,
} from "@calcom/ui";
import { Info, Plus, Trash, MoreHorizontal } from "@calcom/ui/components/icon";
import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
const querySchema = z.object({
schedule: z.coerce.number().positive().optional(),
});
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
@ -93,15 +87,13 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
};
export default function Availability() {
const searchParams = useSearchParams();
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const me = useMeQuery();
const {
data: { schedule: scheduleId },
} = useTypedQuery(querySchema);
const { fromEventType } = router.query;
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(

View File

@ -1,5 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";

View File

@ -4,7 +4,7 @@ import { createEvent } from "ics";
import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
@ -39,6 +39,7 @@ import {
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
@ -47,11 +48,9 @@ import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme, Alert } from "@calcom/ui";
import { X, ExternalLink, ChevronLeft, Check, Calendar } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
import { bookingMetadataSchema, customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -99,6 +98,9 @@ const querySchema = z.object({
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const routerQuery = useRouterQuery();
const pathname = usePathname();
const searchParams = useSearchParams();
const {
allRemainingBookings,
isSuccessBookingPage,
@ -106,7 +108,7 @@ export default function Success(props: SuccessProps) {
formerTime,
email,
seatReferenceUid,
} = querySchema.parse(router.query);
} = querySchema.parse(routerQuery);
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
(attendee) => attendee.email === email
@ -145,24 +147,17 @@ export default function Success(props: SuccessProps) {
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
function setIsCancellationMode(value: boolean) {
const query_ = { ...router.query };
const _searchParams = new URLSearchParams(searchParams);
if (value) {
query_.cancel = "true";
_searchParams.set("cancel", "true");
} else {
if (query_.cancel) {
delete query_.cancel;
if (_searchParams.get("cancel")) {
_searchParams.delete("cancel");
}
}
router.replace(
{
pathname: router.pathname,
query: { ...query_ },
},
undefined,
{ scroll: false }
);
router.replace(`${pathname}?${_searchParams.toString()}`);
}
let evtName = props.eventType.eventName;

View File

@ -1,6 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { z } from "zod";
@ -9,6 +8,7 @@ import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
@ -47,9 +47,9 @@ const querySchema = z.object({
});
export default function Bookings() {
const params = useParamsWithFallback();
const { data: filterQuery } = useFilterQuery();
const router = useRouter();
const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const };
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
const { t } = useLocale();
const query = trpc.viewer.bookings.get.useInfiniteQuery(
@ -62,7 +62,7 @@ export default function Bookings() {
},
{
// first render has status `undefined`
enabled: router.isReady,
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);

View File

@ -2,9 +2,9 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { FC } from "react";
import { useEffect, useState, memo } from "react";
import { memo, useEffect, useState } from "react";
import { z } from "zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
@ -19,18 +19,21 @@ import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Alert,
Avatar,
AvatarGroup,
Badge,
Button,
ButtonGroup,
ConfirmationDialogContent,
CreateButton,
Dialog,
Dropdown,
DropdownItem,
@ -40,15 +43,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
HeadSeo,
HorizontalTabs,
Label,
showToast,
Skeleton,
Switch,
Tooltip,
CreateButton,
HorizontalTabs,
HeadSeo,
Skeleton,
Label,
Alert,
} from "@calcom/ui";
import {
ArrowDown,
@ -63,8 +64,8 @@ import {
MoreHorizontal,
Trash,
Upload,
Users,
User as UserIcon,
Users,
} from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
@ -202,6 +203,8 @@ const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -292,25 +295,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const query = {
...router.query,
dialog: "duplicate",
title: eventType.title,
description: eventType.description,
slug: eventType.slug,
id: eventType.id,
length: eventType.length,
pageSlug: group.profile.slug,
};
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
const newSearchParams = new URLSearchParams(searchParams);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value) newSearchParams.set(key, value.toString());
if (value === null) newSearchParams.delete(key);
}
setParamsIfDefined("dialog", "duplicate");
setParamsIfDefined("title", eventType.title);
setParamsIfDefined("description", eventType.description);
setParamsIfDefined("slug", eventType.slug);
setParamsIfDefined("id", eventType.id);
setParamsIfDefined("length", eventType.length);
setParamsIfDefined("pageSlug", group.profile.slug);
router.push(`${pathname}?${newSearchParams.toString()}`);
};
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@ -845,8 +842,7 @@ const Main = ({
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
}) => {
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
const searchParams = useSearchParams();
const orgBranding = useOrgBranding();
if (!data || status === "loading") {
@ -897,20 +893,20 @@ const Main = ({
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
{router.query.dialog === "duplicate" && <DuplicateDialog />}
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
</>
);
};
const EventTypesPage = () => {
const { t } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const { open } = useIntercom();
const { query } = router;
const { data: user } = useMeQuery();
const [showProfileBanner, setShowProfileBanner] = useState(false);
const orgBranding = useOrgBranding();
const filters = getTeamsFiltersFromQuery(router.query);
const routerQuery = useRouterQuery();
const filters = getTeamsFiltersFromQuery(routerQuery);
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
@ -926,7 +922,7 @@ const EventTypesPage = () => {
}
useEffect(() => {
if (query?.openIntercom && query?.openIntercom === "true") {
if (searchParams?.get("openIntercom") === "true") {
open();
}
setShowProfileBanner(

View File

@ -1,7 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head";
import { useRouter } from "next/router";
import { usePathname, useRouter } from "next/navigation";
import type { CSSProperties } from "react";
import { Suspense } from "react";
import { z } from "zod";
@ -9,6 +9,7 @@ import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc";
import { Button, StepCard, Steps } from "@calcom/ui";
@ -47,11 +48,12 @@ const stepRouteSchema = z.object({
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
const OnboardingPage = () => {
const pathname = usePathname();
const params = useParamsWithFallback();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
const result = stepRouteSchema.safeParse(router.query);
const result = stepRouteSchema.safeParse(params);
const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
@ -96,12 +98,7 @@ const OnboardingPage = () => {
const goToIndex = (index: number) => {
const newStep = steps[index];
router.push(
{
pathname: `/getting-started/${stepTransform(newStep)}`,
},
undefined
);
router.push(`/getting-started/${stepTransform(newStep)}`);
};
const currentStepIndex = steps.indexOf(currentStep);
@ -118,7 +115,7 @@ const OnboardingPage = () => {
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
key={router.asPath}>
key={pathname}>
<Head>
<title>{`${APP_NAME} - ${t("getting_started")}`}</title>
<link rel="icon" href="/favicon.ico" />

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { useIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
@ -33,10 +33,10 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
};
const BillingView = () => {
const pathname = usePathname();
const { t } = useLocale();
const { open } = useIntercom();
const router = useRouter();
const returnTo = router.asPath;
const returnTo = pathname;
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
const onContactSupportClick = async () => {

View File

@ -1,6 +1,6 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Fragment } from "react";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";

View File

@ -1,9 +1,8 @@
import { useRouter } from "next/router";
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localeOptions } from "@calcom/lib/i18n";
import { nameOfDay } from "@calcom/lib/weekday";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
@ -68,7 +67,6 @@ const GeneralQueryView = () => {
};
const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
const router = useRouter();
const utils = trpc.useContext();
const { t } = useLocale();
@ -87,13 +85,6 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
},
});
const localeOptions = useMemo(() => {
return (router.locales || []).map((locale) => ({
value: locale,
label: new Intl.DisplayNames(locale, { type: "language" }).of(locale) || "",
}));
}, [router.locales]);
const timeFormatOptions = [
{ value: 12, label: t("12_hour") },
{ value: 24, label: t("24_hour") },

View File

@ -1,8 +1,6 @@
import { useRouter } from "next/router";
import { AboutOrganizationForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -10,8 +8,6 @@ export { getServerSideProps } from "@calcom/features/ee/organizations/pages/orga
const AboutOrganizationPage = () => {
const { t } = useLocale();
const router = useRouter();
if (!router.isReady) return null;
return (
<>
<Meta title={t("about_your_organization")} description={t("about_your_organization_description")} />

View File

@ -1,9 +1,8 @@
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { AppProps as NextAppProps } from "next/app";
import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -11,8 +10,6 @@ export { getServerSideProps } from "@calcom/features/ee/organizations/pages/orga
const AddNewTeamsPage = () => {
const { t } = useLocale();
const router = useRouter();
if (!router.isReady) return null;
return (
<>
<Meta title={t("create_your_teams")} description={t("create_your_teams_description")} />
@ -21,7 +18,7 @@ const AddNewTeamsPage = () => {
);
};
AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextRouter) => (
AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => (
<>
<WizardLayout
currentStep={5}

View File

@ -1,9 +1,8 @@
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { AppProps as NextAppProps } from "next/app";
import { AddNewOrgAdminsForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -11,8 +10,7 @@ export { getServerSideProps } from "@calcom/features/ee/organizations/pages/orga
const OnboardTeamMembersPage = () => {
const { t } = useLocale();
const router = useRouter();
if (!router.isReady) return null;
return (
<>
<Meta
@ -24,7 +22,7 @@ const OnboardTeamMembersPage = () => {
);
};
OnboardTeamMembersPage.getLayout = (page: React.ReactElement, router: NextRouter) => (
OnboardTeamMembersPage.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => (
<WizardLayout
currentStep={4}
maxSteps={5}

View File

@ -1,8 +1,6 @@
import { useRouter } from "next/router";
import { SetPasswordForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -10,8 +8,6 @@ export { getServerSideProps } from "@calcom/features/ee/organizations/pages/orga
const SetPasswordPage = () => {
const { t } = useLocale();
const router = useRouter();
if (!router.isReady) return null;
return (
<>
<Meta title={t("set_a_password")} description={t("set_a_password_description")} />

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import type { CSSProperties } from "react";
import type { SubmitHandler } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
@ -40,10 +40,10 @@ type FormValues = z.infer<typeof signupSchema>;
type SignupProps = inferSSRProps<typeof getServerSideProps>;
export default function Signup({ prepopulateFormValues, token, orgSlug }: SignupProps) {
const { t, i18n } = useLocale();
const router = useRouter();
const flags = useFlagMap();
const searchParams = useSearchParams();
const telemetry = useTelemetry();
const { t, i18n } = useLocale();
const flags = useFlagMap();
const methods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues,
@ -79,8 +79,8 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
await signIn<"credentials">("credentials", {
...data,
callbackUrl: `${
router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
}?from=signup`,
});
@ -160,8 +160,8 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
className="w-full justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
callbackUrl: searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/getting-started`,
})
}>

View File

@ -1,7 +1,7 @@
import classNames from "classnames";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
@ -10,6 +10,7 @@ import EventTypeDescription from "@calcom/features/eventtypes/components/EventTy
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
@ -32,11 +33,12 @@ export type PageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }: PageProps) {
useTheme(team.theme);
const routerQuery = useRouterQuery();
const pathname = usePathname();
const showMembers = useToggleQuery("members");
const { t } = useLocale();
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
const router = useRouter();
const teamName = team.name || "Nameless Team";
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
const metadata = teamMetadataSchema.parse(team.metadata);
@ -46,7 +48,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
telemetryEventTypes.pageView,
collectPageParameters("/team/[slug]", { isTeamBooking: true })
);
}, [telemetry, router.asPath]);
}, [telemetry, pathname]);
if (isUnpublished) {
const slug = team.slug || metadata?.requestedSlug;
@ -61,7 +63,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
}
// slug is a route parameter, we don't want to forward it to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
const EventTypes = () => (
<ul className="border-subtle rounded-md border">
@ -223,8 +225,8 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
members: "1",
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>

View File

@ -76,6 +76,8 @@ test.describe("Event Types tests", () => {
await page.click(`[data-testid=event-type-options-${eventTypeId}]`);
await page.click(`[data-testid=event-type-duplicate-${eventTypeId}]`);
// Wait for the dialog to appear so we can get the URL
await page.waitForSelector('[data-testid="dialog-title"]');
const url = page.url();
const params = new URLSearchParams(url);

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,5 +1,5 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,9 +1,7 @@
import type { App_RoutingForms_Form } from "@prisma/client";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { createContext, forwardRef, useContext, useState } from "react";
import { useForm } from "react-hook-form";
import { Controller } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
@ -11,6 +9,7 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui";
import {
@ -25,11 +24,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
Form,
SettingsToggle,
showToast,
Switch,
TextAreaField,
TextField,
SettingsToggle,
} from "@calcom/ui";
import { MoreHorizontal } from "@calcom/ui/components/icon";
@ -45,23 +44,23 @@ const newFormModalQuerySchema = z.object({
target: z.string().optional(),
});
const openModal = (router: NextRouter, option: z.infer<typeof newFormModalQuerySchema>) => {
const query = {
...router.query,
dialog: "new-form",
...option,
export const useOpenModal = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const openModal = (option: z.infer<typeof newFormModalQuerySchema>) => {
const newQuery = new URLSearchParams(searchParams);
newQuery.set("dialog", "new-form");
Object.keys(option).forEach((key) => {
newQuery.set(key, option[key as keyof typeof option] || "");
});
router.push(`${pathname}?${newQuery.toString()}`);
};
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
return openModal;
};
function NewFormDialog({ appUrl }: { appUrl: string }) {
const routerQuery = useRouterQuery();
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
@ -84,7 +83,7 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
shouldConnect: boolean;
}>();
const { action, target } = router.query as z.infer<typeof newFormModalQuerySchema>;
const { action, target } = routerQuery as z.infer<typeof newFormModalQuerySchema>;
const formToDuplicate = action === "duplicate" ? target : null;
const teamId = action === "new" ? Number(target) : null;
@ -158,7 +157,6 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
}
const dropdownCtx = createContext<{ dropdown: boolean }>({ dropdown: false });
export const FormActionsDropdown = ({
children,
disabled,
@ -196,6 +194,7 @@ function Dialogs({
deleteDialogFormId: string | null;
}) {
const utils = trpc.useContext();
const router = useRouter();
const { t } = useLocale();
const deleteMutation = trpc.viewer.appRoutingForms.deleteForm.useMutation({
onMutate: async ({ id: formId }) => {
@ -216,6 +215,7 @@ function Dialogs({
onSuccess: () => {
showToast(t("form_deleted"), "success");
setDeleteDialogOpen(false);
router.push(`${appUrl}/forms`);
},
onSettled: () => {
utils.viewer.appRoutingForms.forms.invalidate();
@ -412,7 +412,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
});
const { t } = useLocale();
const router = useRouter();
const openModal = useOpenModal();
const actionData: Record<
FormActionType,
ButtonProps & { as?: React.ElementType; render?: FormActionProps<unknown>["render"] }
@ -427,7 +427,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
},
},
duplicate: {
onClick: () => openModal(router, { action: "duplicate", target: routingForm?.id }),
onClick: () => openModal({ action: "duplicate", target: routingForm?.id }),
},
embed: {
as: EmbedButton,
@ -446,7 +446,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
loading: _delete.isLoading,
},
create: {
onClick: () => createAction({ router, teamId: null }),
onClick: () => openModal({ action: "new", target: "" }),
},
copyRedirectUrl: {
onClick: () => {
@ -486,7 +486,6 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
...action,
...(additionalProps as ButtonProps),
} as ButtonProps & { render?: FormActionProps<unknown>["render"] };
if (actionProps.render) {
return actionProps.render({
routingForm,
@ -517,7 +516,3 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
</DropdownMenuItem>
);
});
export const createAction = ({ router, teamId }: { router: NextRouter; teamId: number | null }) => {
openModal(router, { action: "new", target: teamId ? String(teamId) : "" });
};

View File

@ -1,5 +1,4 @@
// TODO: i18n
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useFormContext } from "react-hook-form";
@ -14,6 +13,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import useApp from "@calcom/lib/hooks/useApp";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import type {
AppGetServerSidePropsContext,
@ -21,46 +21,54 @@ import type {
AppSsrInit,
AppUser,
} from "@calcom/types/AppGetServerSideProps";
import { Badge, ButtonGroup, EmptyScreen, List, ListLinkItem, Tooltip, Button } from "@calcom/ui";
import { CreateButtonWithTeamsList } from "@calcom/ui";
import {
GitMerge,
ExternalLink,
Link as LinkIcon,
Edit,
Download,
Code,
Copy,
Trash,
Menu,
MessageCircle,
FileText,
Shuffle,
Badge,
Button,
ButtonGroup,
CreateButtonWithTeamsList,
EmptyScreen,
List,
ListLinkItem,
Tooltip,
} from "@calcom/ui";
import {
BarChart,
CheckCircle,
Code,
Copy,
Download,
Edit,
ExternalLink,
FileText,
GitMerge,
Link as LinkIcon,
Mail,
Menu,
MessageCircle,
Shuffle,
Trash,
} from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import {
createAction,
FormAction,
FormActionsDropdown,
FormActionsProvider,
useOpenModal,
} from "../../components/FormActions";
import type { RoutingFormWithResponseCount } from "../../components/SingleForm";
import { isFallbackRoute } from "../../lib/isFallbackRoute";
function NewFormButton() {
const { t } = useLocale();
const router = useRouter();
const openModal = useOpenModal();
return (
<CreateButtonWithTeamsList
subtitle={t("create_routing_form_on").toUpperCase()}
data-testid="new-routing-form"
createFunction={(teamId) => {
createAction({ router, teamId: teamId ?? null });
openModal({ action: "new", target: teamId ? String(teamId) : "" });
}}
/>
);
@ -68,17 +76,18 @@ function NewFormButton() {
export default function RoutingForms({
appUrl,
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) {
}: inferSSRProps<typeof getServerSideProps> & {
appUrl: string;
}) {
const { t } = useLocale();
const { hasPaidPlan } = useHasPaidPlan();
const router = useRouter();
const routerQuery = useRouterQuery();
const hookForm = useFormContext<RoutingFormWithResponseCount>();
useEffect(() => {
hookForm.reset({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filters = getTeamsFiltersFromQuery(router.query);
const filters = getTeamsFiltersFromQuery(routerQuery);
const queryRes = trpc.viewer.appRoutingForms.forms.useQuery({
filters,

View File

@ -1,9 +1,9 @@
import type { GetServerSidePropsContext } from "next";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { AppProps as NextAppProps } from "next/app";
import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { AppPrisma, AppSsrInit, AppUser } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
@ -15,7 +15,7 @@ type Component = {
default: React.ComponentType & Pick<AppProps["Component"], "getLayout">;
getServerSideProps?: (context: GetServerSidePropsContext, ...rest: GetServerSidePropsRestArgs) => void;
};
const getComponent = (route: string | NextRouter): Component => {
const getComponent = (route: string | NextAppProps["router"]): Component => {
const defaultRoute = "forms";
const routeKey =
typeof route === "string" ? route || defaultRoute : route?.query?.pages?.[0] || defaultRoute;
@ -23,9 +23,9 @@ const getComponent = (route: string | NextRouter): Component => {
};
export default function LayoutHandler(props: { [key: string]: unknown }) {
const params = useParamsWithFallback();
const methods = useForm();
const router = useRouter();
const pageKey = router?.query?.pages?.[0] || "forms";
const pageKey = params?.pages?.[0] || "forms";
const PageComponent = getComponent(pageKey).default;
return (
<FormProvider {...methods}>
@ -34,7 +34,7 @@ export default function LayoutHandler(props: { [key: string]: unknown }) {
);
}
LayoutHandler.getLayout = (page: React.ReactElement, router: NextRouter) => {
LayoutHandler.getLayout = (page: React.ReactElement, router: NextAppProps["router"]) => {
const component = getComponent(router).default;
if (component && "getLayout" in component) {
return component.getLayout?.(page, router);

View File

@ -1,5 +1,5 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import type { FormEvent } from "react";
import { useEffect, useRef, useState } from "react";
import { Toaster } from "react-hot-toast";
@ -295,14 +295,16 @@ export const getServerSideProps = async function getServerSideProps(
};
const usePrefilledResponse = (form: Props["form"]) => {
const router = useRouter();
const searchParams = useSearchParams();
const prefillResponse: Response = {};
// Prefill the form from query params
form.fields?.forEach((field) => {
const valuesFromQuery = searchParams?.getAll(getFieldIdentifier(field)).filter(Boolean);
// We only want to keep arrays if the field is a multi-select
const value = valuesFromQuery.length > 1 ? valuesFromQuery : valuesFromQuery[0];
prefillResponse[field.id] = {
value: router.query[getFieldIdentifier(field)] || "",
value: value || "",
label: field.label,
};
});

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
@ -6,7 +6,7 @@ import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, TextField, Select } from "@calcom/ui";
import { Alert, Select, TextField } from "@calcom/ui";
import { paymentOptions } from "../lib/constants";
import type { appDataSchema } from "../zod";
@ -14,7 +14,7 @@ import type { appDataSchema } from "../zod";
type Option = { value: string; label: string };
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const { asPath } = useRouter();
const pathname = usePathname();
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price");
const currency = getAppData("currency");
@ -37,7 +37,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
.trim();
return (
<AppCard
returnTo={WEBAPP_URL + asPath}
returnTo={WEBAPP_URL + pathname}
setAppData={setAppData}
app={app}
disableSwitch={disabled}

View File

@ -1,6 +1,6 @@
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import type { CSSProperties } from "react";
import { useState, useEffect } from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import type { Message } from "./embed";
import { sdkActionManager } from "./sdk-event";
@ -177,14 +177,35 @@ function isValidNamespace(ns: string | null | undefined) {
return typeof ns !== "undefined" && ns !== null;
}
export const useEmbedTheme = () => {
const router = useRouter();
const [theme, setTheme] = useState(embedStore.theme || (router.query.theme as typeof embedStore.theme));
/**
* It handles any URL change done through Web history API as well
* History API is currenty being used by Booker/utils/query-param
*/
const useUrlChange = (callback: (newUrl: string) => void) => {
const currentFullUrl = isBrowser ? new URL(document.URL) : null;
const pathname = currentFullUrl?.pathname ?? "";
const searchParams = currentFullUrl?.searchParams ?? null;
const lastKnownUrl = useRef(`${pathname}?${searchParams}`);
useEffect(() => {
router.events.on("routeChangeComplete", () => {
sdkActionManager?.fire("__routeChanged", {});
});
}, [router.events]);
const newUrl = `${pathname}?${searchParams}`;
if (lastKnownUrl.current !== newUrl) {
lastKnownUrl.current = newUrl;
callback(newUrl);
}
}, [pathname, searchParams, callback]);
};
export const useEmbedTheme = () => {
const searchParams = useSearchParams();
const [theme, setTheme] = useState(
embedStore.theme || (searchParams?.get("theme") as typeof embedStore.theme)
);
const onUrlChange = useCallback(() => {
sdkActionManager?.fire("__routeChanged", {});
}, []);
useUrlChange(onUrlChange);
embedStore.setTheme = setTheme;
return theme;
};
@ -363,7 +384,6 @@ const methods = {
};
export type InterfaceWithParent = {
// Ensure that only one argument is read by the method
[key in keyof typeof methods]: (firstAndOnlyArg: Parameters<(typeof methods)[key]>[number]) => void;
};

View File

@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { noop } from "lodash";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import type { FC } from "react";
import { useReducer, useState } from "react";
import { Controller, useForm } from "react-hook-form";
@ -27,8 +27,8 @@ import {
SkeletonButton,
SkeletonContainer,
SkeletonText,
TextField,
Switch,
TextField,
} from "@calcom/ui";
import { AlertCircle, Edit } from "@calcom/ui/components/icon";
@ -265,13 +265,13 @@ interface EditModalState extends Pick<App, "keys"> {
}
const AdminAppsListContainer = () => {
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const { category } = querySchema.parse(router.query);
const category = searchParams.get("category") || AppCategories.calendar;
const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery(
{ category },
{ enabled: router.isReady }
{ enabled: searchParams !== null }
);
const [modalState, setModalState] = useReducer(

View File

@ -3,13 +3,7 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui";
import {
Button,
ConfirmationDialogContent,
Dialog,
DialogTrigger,
showToast,
} from "@calcom/ui";
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger, showToast } from "@calcom/ui";
import { Trash } from "@calcom/ui/components/icon";
export default function DisconnectIntegration({

View File

@ -3,7 +3,7 @@ import type { UseMutationResult } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import type { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import type { FieldError } from "react-hook-form";
import { useForm } from "react-hook-form";
@ -14,23 +14,24 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import dayjs from "@calcom/dayjs";
import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog";
import {
useTimePreferences,
mapBookingToMutationInput,
createBooking,
createRecurringBooking,
mapBookingToMutationInput,
mapRecurringBookingToMutationInput,
useTimePreferences,
} from "@calcom/features/bookings/lib";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { getFullName } from "@calcom/features/form-builder/utils";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc";
import { Form, Button, Alert, EmptyScreen, showToast } from "@calcom/ui";
import { Alert, Button, EmptyScreen, Form, showToast } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { useBookerStore } from "../../store";
@ -43,7 +44,10 @@ type BookEventFormProps = {
};
export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
const searchParams = useSearchParams();
const routerQuery = useRouterQuery();
const session = useSession();
const bookingSuccessRedirect = useBookingSuccessRedirect();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation({
trpc: { context: { skipBatch: true } },
});
@ -113,10 +117,10 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
});
const parsedQuery = querySchema.parse({
...router.query,
...routerQuery,
// `guest` because we need to support legacy URL with `guest` query param support
// `guests` because the `name` of the corresponding bookingField is `guests`
guests: router.query.guests || router.query.guest,
guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"),
});
const defaultUserValues = {
@ -204,11 +208,11 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
});
const createBookingMutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
onSuccess: (responseData) => {
const { uid, paymentUid } = responseData;
const fullName = getFullName(bookingForm.getValues("responses.name"));
if (paymentUid) {
return await router.push(
return router.push(
createPaymentLink({
paymentUid,
date: timeslot,
@ -234,7 +238,6 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType?.successRedirectUrl || "",
query,
bookingUid: uid,
@ -264,7 +267,6 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType?.successRedirectUrl || "",
query,
bookingUid: uid,
@ -347,12 +349,12 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
rescheduleUid: rescheduleUid || undefined,
bookingUid: (bookingData && bookingData.uid) || seatedEventData?.bookingUid || undefined,
username: username || "",
metadata: Object.keys(router.query)
metadata: Object.keys(routerQuery)
.filter((key) => key.startsWith("metadata"))
.reduce(
(metadata, key) => ({
...metadata,
[key.substring("metadata[".length, key.length - 1)]: router.query[key],
[key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key),
}),
{}
),
@ -368,7 +370,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
};
if (!eventType) {
console.warn("No event type found for event", router.query);
console.warn("No event type found for event", routerQuery);
return <Alert severity="warning" message={t("error_booking_event")} />;
}

View File

@ -5,10 +5,10 @@ import classNames from "@calcom/lib/classNames";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Clock, CheckSquare, RefreshCcw, CreditCard } from "@calcom/ui/components/icon";
import { AvailableEventLocations } from "@calcom/web/components/booking/AvailableEventLocations";
import type { PublicEvent } from "../../types";
import { EventDetailBlocks } from "../../types";
import { AvailableEventLocations } from "./AvailableEventLocations";
import { EventDuration } from "./Duration";
import { EventOccurences } from "./Occurences";
import { EventPrice } from "./Price";

View File

@ -1,11 +1,12 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import { Avatar, Button, Form, ImageUploader, Alert, Label, TextAreaField } from "@calcom/ui";
import { Alert, Avatar, Button, Form, ImageUploader, Label, TextAreaField } from "@calcom/ui";
import { ArrowRight, Plus } from "@calcom/ui/components/icon";
const querySchema = z.object({
@ -15,7 +16,8 @@ const querySchema = z.object({
export const AboutOrganizationForm = () => {
const { t } = useLocale();
const router = useRouter();
const { id: orgId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { id: orgId } = querySchema.parse(routerQuery);
const [serverErrorMessage, setServerErrorMessage] = useState<string | null>(null);
const [image, setImage] = useState("");

View File

@ -1,12 +1,13 @@
import { ArrowRight } from "lucide-react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast, TextAreaField, Form } from "@calcom/ui";
import { Button, Form, showToast, TextAreaField } from "@calcom/ui";
const querySchema = z.object({
id: z.string().transform((val) => parseInt(val)),
@ -15,7 +16,8 @@ const querySchema = z.object({
export const AddNewOrgAdminsForm = () => {
const { t, i18n } = useLocale();
const router = useRouter();
const { id: orgId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { id: orgId } = querySchema.parse(routerQuery);
const newAdminsFormMethods = useForm<{
emails: string[];
}>();

View File

@ -1,13 +1,14 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast, TextField } from "@calcom/ui";
import { Plus, X, ArrowRight } from "@calcom/ui/components/icon";
import { ArrowRight, Plus, X } from "@calcom/ui/components/icon";
const querySchema = z.object({
id: z.string().transform((val) => parseInt(val)),
@ -26,7 +27,8 @@ const schema = z.object({
export const AddNewTeamsForm = () => {
const { t } = useLocale();
const router = useRouter();
const { id: orgId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { id: orgId } = querySchema.parse(routerQuery);
const [counter, setCounter] = useState(1);
const { register, control, handleSubmit, formState, trigger, setValue, getValues } = useForm({

View File

@ -1,5 +1,5 @@
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";

View File

@ -1,13 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, Alert, PasswordField } from "@calcom/ui";
import { Alert, Button, Form, PasswordField } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
const querySchema = z.object({
@ -33,7 +34,8 @@ const formSchema = z.object({
export const SetPasswordForm = () => {
const { t } = useLocale();
const router = useRouter();
const { id: orgId } = querySchema.parse(router.query);
const routerQuery = useRouterQuery();
const { id: orgId } = querySchema.parse(routerQuery);
const [serverErrorMessage, setServerErrorMessage] = useState<string | null>(null);

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { LinkIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState, useLayoutEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@ -223,24 +223,24 @@ const OrgProfileView = () => {
</div>
)}
{/* Disable Org disbanding */}
{/* <hr className="border-subtle my-8 border" />
<div className="text-default mb-3 text-base font-semibold">{t("danger_zone")}</div>
{currentOrganisation?.user.role === "OWNER" ? (
<Dialog>
<DialogTrigger asChild>
<Button color="destructive" className="border" StartIcon={Trash2}>
{t("disband_org")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("disband_org")}
confirmBtnText={t("confirm")}
onConfirm={deleteTeam}>
{t("disband_org_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
) : null} */}
{/* <hr className="border-subtle my-8 border" />
<div className="text-default mb-3 text-base font-semibold">{t("danger_zone")}</div>
{currentOrganisation?.user.role === "OWNER" ? (
<Dialog>
<DialogTrigger asChild>
<Button color="destructive" className="border" StartIcon={Trash2}>
{t("disband_org")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("disband_org")}
confirmBtnText={t("confirm")}
onConfirm={deleteTeam}>
{t("disband_org_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
) : null} */}
{/* LEAVE ORG should go above here ^ */}
</>
)}

View File

@ -1,14 +1,14 @@
import type { Payment } from "@prisma/client";
import { useElements, useStripe, PaymentElement, Elements } from "@stripe/react-stripe-js";
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
import type stripejs from "@stripe/stripe-js";
import type { StripeElementLocale } from "@stripe/stripe-js";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import type { SyntheticEvent } from "react";
import { useEffect, useState } from "react";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import type { StripePaymentData, StripeSetupIntentData } from "@calcom/app-store/stripepayment/lib/server";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, CheckboxField } from "@calcom/ui";
@ -35,11 +35,13 @@ type States =
const PaymentForm = (props: Props) => {
const { t, i18n } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const [state, setState] = useState<States>({ status: "idle" });
const stripe = useStripe();
const elements = useElements();
const paymentOption = props.payment.paymentOption;
const [holdAcknowledged, setHoldAcknowledged] = useState<boolean>(paymentOption === "HOLD" ? false : true);
const bookingSuccessRedirect = useBookingSuccessRedirect();
useEffect(() => {
elements?.update({ locale: i18n.language as StripeElementLocale });
@ -48,13 +50,13 @@ const PaymentForm = (props: Props) => {
const handleSubmit = async (ev: SyntheticEvent) => {
ev.preventDefault();
if (!stripe || !elements || !router.isReady) return;
if (!stripe || !elements) return;
setState({ status: "processing" });
let payload;
const params: { [k: string]: any } = {
uid: props.bookingUid,
email: router.query.email,
email: searchParams.get("email"),
};
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
payload = await stripe.confirmSetup({
@ -86,7 +88,6 @@ const PaymentForm = (props: Props) => {
}
return bookingSuccessRedirect({
router,
successRedirectUrl: props.eventType.successRedirectUrl,
query: params,
bookingUid: props.bookingUid,

View File

@ -1,20 +1,21 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { trpc } from "@calcom/trpc/react";
import { AppSkeletonLoader as SkeletonLoader } from "@calcom/ui";
import { Meta } from "@calcom/ui";
import { AppSkeletonLoader as SkeletonLoader, Meta } from "@calcom/ui";
import { getLayout } from "../../../settings/layouts/SettingsLayout";
import SSOConfiguration from "../components/SSOConfiguration";
const SAMLSSO = () => {
const params = useParamsWithFallback();
const { t } = useLocale();
const router = useRouter();
const teamId = Number(router.query.id);
const teamId = Number(params.id);
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery(
{ teamId },

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";

View File

@ -1,5 +1,5 @@
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { z } from "zod";
@ -33,10 +33,13 @@ type FormValues = {
};
const AddNewTeamMembers = () => {
const searchParams = useSearchParams();
const session = useSession();
const router = useRouter();
const { id: teamId } = router.isReady ? querySchema.parse(router.query) : { id: -1 };
const teamQuery = trpc.viewer.teams.get.useQuery({ teamId }, { enabled: router.isReady });
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
const teamQuery = trpc.viewer.teams.get.useQuery(
{ teamId },
{ enabled: session.status === "authenticated" }
);
if (session.status === "loading" || !teamQuery.data) return <AddNewTeamMemberSkeleton />;
return <AddNewTeamMembersForm defaultValues={{ members: teamQuery.data.members }} teamId={teamId} />;
@ -49,16 +52,17 @@ export const AddNewTeamMembersForm = ({
defaultValues: FormValues;
teamId: number;
}) => {
const searchParams = useSearchParams();
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const showDialog = router.query.inviteModal === "true";
const showDialog = searchParams?.get("inviteModal") === "true";
const [memberInviteModal, setMemberInviteModal] = useState(showDialog);
const [inviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false);
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery({ teamId });
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery({ teamId }, { enabled: !!teamId });
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@ -6,6 +6,7 @@ import { z } from "zod";
import { extractDomainFromWebsiteUrl } from "@calcom/ee/organizations/lib/utils";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import slugify from "@calcom/lib/slugify";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
@ -24,7 +25,8 @@ export const CreateANewTeamForm = () => {
const { t } = useLocale();
const router = useRouter();
const telemetry = useTelemetry();
const parsedQuery = querySchema.safeParse(router.query);
const params = useParamsWithFallback();
const parsedQuery = querySchema.safeParse(params);
const [serverErrorMessage, setServerErrorMessage] = useState<string | null>(null);
const orgBranding = useOrgBranding();

View File

@ -1,12 +1,12 @@
import { UsersIcon, XIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import type { PropsWithChildren } from "react";
import { useState } from "react";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { trpc } from "@calcom/trpc";
import { Button, Tooltip, showToast } from "@calcom/ui";
import { Button, showToast, Tooltip } from "@calcom/ui";
const GoogleIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -35,11 +35,11 @@ function gotoUrl(url: string, newTab?: boolean) {
export function GoogleWorkspaceInviteButton(
props: PropsWithChildren<{ onSuccess: (data: string[]) => void }>
) {
const router = useRouter();
const featureFlags = useFlagMap();
const utils = trpc.useContext();
const { t } = useLocale();
const teamId = Number(router.query.id);
const params = useParamsWithFallback();
const teamId = Number(params.id);
const [googleWorkspaceLoading, setGoogleWorkspaceLoading] = useState(false);
const { data: credential } = trpc.viewer.googleWorkspace.checkForGWorkspace.useQuery();
const { data: hasGcalInstalled } = trpc.viewer.appsRouter.checkGlobalKeys.useQuery({

View File

@ -1,5 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState } from "react";
import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSettingsModal";
@ -52,13 +53,12 @@ interface Props {
}
export default function TeamListItem(props: Props) {
const searchParams = useSearchParams();
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const team = props.team;
const showDialog = router.query.inviteModal === "true";
const showDialog = searchParams?.get("inviteModal") === "true";
const [openMemberInvitationModal, setOpenMemberInvitationModal] = useState(showDialog);
const [openInviteLinkSettingsModal, setOpenInviteLinkSettingsModal] = useState(false);

Some files were not shown because too many files have changed in this diff Show More