diff --git a/apps/web/components/Shell.tsx b/apps/web/components/Shell.tsx
index 15efb679..b0e7a75d 100644
--- a/apps/web/components/Shell.tsx
+++ b/apps/web/components/Shell.tsx
@@ -1,23 +1,25 @@
import { SelectorIcon } from "@heroicons/react/outline";
import {
+ ArrowLeftIcon,
CalendarIcon,
ClockIcon,
CogIcon,
ExternalLinkIcon,
LinkIcon,
LogoutIcon,
- ViewGridIcon,
- MoonIcon,
MapIcon,
- ArrowLeftIcon,
+ MoonIcon,
+ ViewGridIcon,
} from "@heroicons/react/solid";
-import { signOut, useSession } from "next-auth/react";
+import { SessionContextValue, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, ReactNode, useEffect } from "react";
import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { UserPlan } from "@calcom/prisma/client";
import Button from "@calcom/ui/Button";
import Dropdown, {
DropdownMenuContent,
@@ -30,9 +32,8 @@ import TrialBanner from "@ee/components/TrialBanner";
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import classNames from "@lib/classNames";
-import { NEXT_PUBLIC_BASE_URL } from "@lib/config/constants";
+import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
-import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
@@ -58,7 +59,6 @@ function useRedirectToLoginIfUnauthenticated(isPublic = false) {
const { data: session, status } = useSession();
const loading = status === "loading";
const router = useRouter();
- const shouldDisplayUnauthed = router.pathname.startsWith("/apps");
useEffect(() => {
if (isPublic) {
@@ -69,7 +69,7 @@ function useRedirectToLoginIfUnauthenticated(isPublic = false) {
router.replace({
pathname: "/auth/login",
query: {
- callbackUrl: `${NEXT_PUBLIC_BASE_URL}/${location.pathname}${location.search}`,
+ callbackUrl: `${WEBAPP_URL}/${location.pathname}${location.search}`,
},
});
}
@@ -78,7 +78,6 @@ function useRedirectToLoginIfUnauthenticated(isPublic = false) {
return {
loading: loading && !session,
- shouldDisplayUnauthed,
session,
};
}
@@ -122,28 +121,14 @@ export function ShellSubHeading(props: {
);
}
-export default function Shell(props: {
- centered?: boolean;
- title?: string;
- heading?: ReactNode;
- subtitle?: ReactNode;
- children: ReactNode;
- CTA?: ReactNode;
- large?: boolean;
- HeadingLeftIcon?: ReactNode;
- backPath?: string; // renders back button to specified path
- // use when content needs to expand with flex
- flexChildrenContainer?: boolean;
- isPublic?: boolean;
-}) {
+const Layout = ({
+ status,
+ plan,
+ ...props
+}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan }) => {
const isEmbed = useIsEmbed();
- const { t } = useLocale();
const router = useRouter();
- const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
- const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
-
- const telemetry = useTelemetry();
-
+ const { t } = useLocale();
const navigation = [
{
name: t("event_types_page_title"),
@@ -188,35 +173,10 @@ export default function Shell(props: {
current: router.asPath.startsWith("/settings"),
},
];
-
- useEffect(() => {
- telemetry.withJitsu((jitsu) => {
- return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
- });
- }, [telemetry, router.asPath]);
-
const pageTitle = typeof props.heading === "string" ? props.heading : props.title;
- const query = useMeQuery();
- const user = query.data;
-
- const i18n = useViewerI18n();
- const { status } = useSession();
-
- if (i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading) {
- // show spinner whilst i18n is loading to avoid language flicker
- return (
-
-
-
- );
- }
-
- if (!session && !props.isPublic) return null;
-
return (
<>
-
© {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
{process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"}
-
- -{user && user.plan}
+
+ -{plan}
@@ -426,6 +386,60 @@ export default function Shell(props: {
>
);
+};
+
+const MemoizedLayout = React.memo(Layout);
+
+type LayoutProps = {
+ centered?: boolean;
+ title?: string;
+ heading?: ReactNode;
+ subtitle?: ReactNode;
+ children: ReactNode;
+ CTA?: ReactNode;
+ large?: boolean;
+ HeadingLeftIcon?: ReactNode;
+ backPath?: string; // renders back button to specified path
+ // use when content needs to expand with flex
+ flexChildrenContainer?: boolean;
+ isPublic?: boolean;
+};
+
+export default function Shell(props: LayoutProps) {
+ const router = useRouter();
+ const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
+ const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
+ const telemetry = useTelemetry();
+
+ useEffect(() => {
+ telemetry.withJitsu((jitsu) => {
+ return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
+ });
+ }, [telemetry, router.asPath]);
+
+ const query = useMeQuery();
+ const user = query.data;
+
+ const i18n = useViewerI18n();
+ const { status } = useSession();
+
+ if (i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading) {
+ // show spinner whilst i18n is loading to avoid language flicker
+ return (
+
+
+
+ );
+ }
+
+ if (!session && !props.isPublic) return null;
+
+ return (
+ <>
+
+
+ >
+ );
}
function UserDropdown({ small }: { small?: boolean }) {
diff --git a/apps/web/lib/QueryCell.tsx b/apps/web/lib/QueryCell.tsx
index f3dce5a9..79a7d7db 100644
--- a/apps/web/lib/QueryCell.tsx
+++ b/apps/web/lib/QueryCell.tsx
@@ -9,8 +9,21 @@ import {
import { Alert } from "@calcom/ui/Alert";
+import { trpc } from "@lib/trpc";
+
import Loader from "@components/Loader";
+import type { AppRouter } from "@server/routers/_app";
+import type { TRPCClientErrorLike } from "@trpc/client";
+import type { UseTRPCQueryOptions } from "@trpc/react";
+// import type { inferProcedures } from "@trpc/react/src/createReactQueryHooks";
+import type {
+ inferHandlerInput,
+ inferProcedureInput,
+ inferProcedureOutput,
+ ProcedureRecord,
+} from "@trpc/server";
+
type ErrorLike = {
message: string;
};
@@ -72,3 +85,31 @@ export function QueryCell(
// impossible state
return null;
}
+
+type inferProcedures> = {
+ [TPath in keyof TObj]: {
+ input: inferProcedureInput;
+ output: inferProcedureOutput;
+ };
+};
+type TQueryValues = inferProcedures;
+type TQueries = AppRouter["_def"]["queries"];
+type TError = TRPCClientErrorLike;
+
+const withQuery = (
+ pathAndInput: [path: TPath, ...args: inferHandlerInput],
+ params?: UseTRPCQueryOptions
+) => {
+ return function WithQuery(
+ opts: Omit<
+ Partial> &
+ QueryCellOptionsNoEmpty,
+ "query"
+ >
+ ) {
+ const query = trpc.useQuery(pathAndInput, params);
+ return ;
+ };
+};
+
+export { withQuery };
diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx
index bcefe1d1..01880eae 100644
--- a/apps/web/pages/availability/index.tsx
+++ b/apps/web/pages/availability/index.tsx
@@ -9,7 +9,7 @@ import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
-import { QueryCell } from "@lib/QueryCell";
+import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
@@ -99,13 +99,14 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
);
}
+const WithQuery = withQuery(["viewer.availability.list"]);
+
export default function AvailabilityPage() {
const { t } = useLocale();
- const query = trpc.useQuery(["viewer.availability.list"]);
return (
);
diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx
index cbc04b83..86697910 100644
--- a/apps/web/pages/event-types/index.tsx
+++ b/apps/web/pages/event-types/index.tsx
@@ -30,7 +30,7 @@ import Dropdown, {
DropdownMenuSeparator,
} from "@calcom/ui/Dropdown";
-import { QueryCell } from "@lib/QueryCell";
+import { withQuery } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
@@ -63,38 +63,83 @@ type EventTypeGroup = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"][n
type EventType = EventTypeGroup["eventTypes"][number];
interface EventTypeListProps {
group: EventTypeGroup;
+ groupIndex: number;
readOnly: boolean;
types: EventType[];
}
-export const EventTypeList = ({ group, readOnly, types }: EventTypeListProps): JSX.Element => {
+const Item = ({ type, group, readOnly }: any) => {
+ const { t } = useLocale();
+
+ return (
+
+
+
+
+ {type.title}
+
+ {`/${group.profile.slug}/${type.slug}`}
+ {type.hidden && (
+
+ {t("hidden")}
+
+ )}
+ {readOnly && (
+
+ {t("readonly")}
+
+ )}
+
+
+
+
+ );
+};
+
+const MemoizedItem = React.memo(Item);
+
+export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const mutation = trpc.useMutation("viewer.eventTypeOrder", {
- onError: (err) => {
+ onError: async (err) => {
console.error(err.message);
- },
- async onSettled() {
await utils.cancelQuery(["viewer.eventTypes"]);
await utils.invalidateQueries(["viewer.eventTypes"]);
},
});
- const [sortableTypes, setSortableTypes] = useState(types);
- useEffect(() => {
- setSortableTypes(types);
- }, [types]);
- function moveEventType(index: number, increment: 1 | -1) {
- const newList = [...sortableTypes];
- const type = sortableTypes[index];
- const tmp = sortableTypes[index + increment];
+ function moveEventType(index: number, increment: 1 | -1) {
+ const newList = [...types];
+
+ const type = types[index];
+ const tmp = types[index + increment];
if (tmp) {
newList[index] = tmp;
newList[index + increment] = type;
}
- setSortableTypes(newList);
+
+ utils.cancelQuery(["viewer.eventTypes"]);
+ utils.setQueryData(["viewer.eventTypes"], (data) =>
+ Object.assign(data, {
+ eventTypesGroups: [
+ data?.eventTypeGroups.slice(0, groupIndex),
+ Object.assign(group, {
+ eventTypes: newList,
+ }),
+ data?.eventTypeGroups.slice(groupIndex + 1),
+ ],
+ })
+ );
+
mutation.mutate({
ids: newList.map((type) => type.id),
});
@@ -155,7 +200,7 @@ export const EventTypeList = ({ group, readOnly, types }: EventTypeListProps): J
return (
- {sortableTypes.map((type, index) => (
+ {types.map((type, index) => (
-
- {sortableTypes.length > 1 && (
+ {types.length > 1 && (
<>