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:
parent
4ca9138e01
commit
b7851e6e53
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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 we’ll 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")}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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="">
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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: "/",
|
||||
|
|
|
@ -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"] = [
|
||||
{
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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") },
|
||||
|
|
|
@ -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")} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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")} />
|
||||
|
|
|
@ -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`,
|
||||
})
|
||||
}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) : "" });
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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")} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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("");
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}>();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 ^ */}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user