Skip to content

Commit

Permalink
add tRPC (calcom#614)
Browse files Browse the repository at this point in the history
* add trpc

* trpc specific

* fix deps

* lint fix

* upgrade prisma

* nativeTypes

* nope, not needed

* fix app propviders

* Revert "upgrade prisma"

This reverts commit e6f2d25.

* rev

* up trpc

* simplify

* wip - bookings page with trpc

* bookings using trpc

* fix `Shell` props

* call it viewerRouter instead

* cleanuop

* ssg helper

* fix lint

* fix types

* skip

* add `useRedirectToLoginIfUnauthenticated`

* exhaustive-deps

* fix callbackUrl

* rewrite `/availability` using trpc

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
KATT and kodiakhq[bot] authored Sep 27, 2021
1 parent 0938f6f commit 3430065
Show file tree
Hide file tree
Showing 18 changed files with 483 additions and 216 deletions.
82 changes: 59 additions & 23 deletions components/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: replace headlessui with radix-ui
import { Menu, Transition } from "@headlessui/react";
import { SelectorIcon } from "@heroicons/react/outline";
import {
Expand All @@ -10,28 +9,69 @@ import {
LogoutIcon,
PuzzleIcon,
} from "@heroicons/react/solid";
import { User } from "@prisma/client";
import { signOut, useSession } from "next-auth/client";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import React, { Fragment, ReactNode, useEffect } from "react";
import { Toaster } from "react-hot-toast";

import HelpMenuItemDynamic from "@ee/lib/intercom/HelpMenuItemDynamic";

import classNames from "@lib/classNames";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";

import { HeadSeo } from "@components/seo/head-seo";
import Avatar from "@components/ui/Avatar";

import Loader from "./Loader";
import Logo from "./Logo";

export default function Shell(props) {
function useMeQuery() {
const [session] = useSession();
const meQuery = trpc.useQuery(["viewer.me"], {
// refetch max once per 5s
staleTime: 5000,
});

useEffect(() => {
// refetch if sesion changes
meQuery.refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session]);

return meQuery;
}

function useRedirectToLoginIfUnauthenticated() {
const [session, loading] = useSession();
const router = useRouter();

useEffect(() => {
if (!loading && !session) {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `${location.pathname}${location.search}`,
},
});
}
}, [loading, session, router]);
}

export default function Shell(props: {
title?: string;
heading: ReactNode;
subtitle: string;
children: ReactNode;
CTA?: ReactNode;
}) {
const router = useRouter();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [session, loading] = useSession();
useRedirectToLoginIfUnauthenticated();

const telemetry = useTelemetry();
const query = useMeQuery();

const navigation = [
{
Expand Down Expand Up @@ -72,16 +112,19 @@ export default function Shell(props) {
});
}, [telemetry]);

if (!loading && !session) {
if (query.status !== "loading" && !query.data) {
router.replace("/auth/login");
}

const pageTitle = typeof props.heading === "string" ? props.heading : props.title;
if (query.status === "loading") {
return <Loader />;
}

return session ? (
return (
<>
<HeadSeo
title={pageTitle}
title={pageTitle ?? "Cal.com"}
description={props.subtitle}
nextSeoProps={{
nofollow: true,
Expand Down Expand Up @@ -155,7 +198,7 @@ export default function Shell(props) {
</Link>
</button>
<div className="mt-1">
<UserDropdown small bottom session={session} />
<UserDropdown small bottom />
</div>
</div>
</nav>
Expand Down Expand Up @@ -206,19 +249,12 @@ export default function Shell(props) {
</div>
</div>
</>
) : null;
);
}

function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean }) {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
fetch("/api/me")
.then((res) => res.json())
.then((responseBody) => {
setUser(responseBody.user);
});
}, []);
const query = useMeQuery();
const user = query.data;

return (
<Menu as="div" className="w-full relative inline-block text-left">
Expand All @@ -230,18 +266,18 @@ function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean })
<span className="flex w-full justify-between items-center">
<span className="flex min-w-0 items-center justify-between space-x-3">
<Avatar
imageSrc={user?.avatar}
displayName={user?.name}
imageSrc={user.avatar}
alt={user.username}
className={classNames(
small ? "w-8 h-8" : "w-10 h-10",
"bg-gray-300 rounded-full flex-shrink-0"
)}
/>
{!small && (
<span className="flex-1 flex flex-col min-w-0">
<span className="text-gray-900 text-sm font-medium truncate">{user?.name}</span>
<span className="text-gray-900 text-sm font-medium truncate">{user.name}</span>
<span className="text-neutral-500 font-normal text-sm truncate">
/{user?.username}
/{user.username}
</span>
</span>
)}
Expand Down
12 changes: 7 additions & 5 deletions components/ui/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Maybe } from "@trpc/server";

import classNames from "@lib/classNames";
import { defaultAvatarSrc } from "@lib/profile";

export type AvatarProps = {
className?: string;
size: number;
imageSrc?: string;
size?: number;
imageSrc?: Maybe<string>;
title?: string;
alt: string;
gravatarFallbackMd5?: string;
};

export default function Avatar({ imageSrc, gravatarFallbackMd5, size, alt, title, ...props }: AvatarProps) {
const className = classNames("rounded-full", props.className, `h-${size} w-${size}`);
export default function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
const avatar = (
<AvatarPrimitive.Root>
<AvatarPrimitive.Image
src={imageSrc}
src={imageSrc ?? undefined}
alt={alt}
className={classNames("rounded-full", `h-auto w-${size}`, props.className)}
/>
Expand Down
4 changes: 2 additions & 2 deletions cypress/integration/cancel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
describe("cancel", () => {
describe.skip("cancel", () => {
describe("Admin user can cancel events", () => {
before(() => {
cy.visit("/bookings");
cy.login("[email protected]", "pro");
});
it.skip("can cancel bookings", () => {
it("can cancel bookings", () => {
cy.visit("/bookings");
cy.get("[data-testid=bookings]").children().should("have.length.at.least", 1);
cy.get("[data-testid=cancel]").click();
Expand Down
65 changes: 42 additions & 23 deletions lib/app-providers.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
import { IdProvider } from "@radix-ui/react-id";
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
import { loggerLink } from "@trpc/client/links/loggerLink";
import { withTRPC } from "@trpc/next";
import { Provider } from "next-auth/client";
import { AppProps } from "next/dist/shared/lib/router/router";
import React from "react";
import { HydrateProps, QueryClient, QueryClientProvider } from "react-query";
import { Hydrate } from "react-query/hydration";

import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic";

import { Session } from "@lib/auth";
import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry";

export const queryClient = new QueryClient();

type AppProviderProps = {
pageProps: {
session?: Session;
dehydratedState?: HydrateProps;
};
};

const AppProviders: React.FC<AppProviderProps> = ({ pageProps, children }) => {
const AppProviders = (props: AppProps) => {
return (
<TelemetryProvider value={createTelemetryClient()}>
<QueryClientProvider client={queryClient}>
<IdProvider>
<DynamicIntercomProvider>
<Hydrate state={pageProps.dehydratedState}>
<Provider session={pageProps.session}>{children}</Provider>
</Hydrate>
</DynamicIntercomProvider>
</IdProvider>
</QueryClientProvider>
<IdProvider>
<DynamicIntercomProvider>
<Provider session={props.pageProps.session}>{props.children}</Provider>
</DynamicIntercomProvider>
</IdProvider>
</TelemetryProvider>
);
};

export default AppProviders;
export default withTRPC({
config() {
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
return {
/**
* @link https://trpc.io/docs/links
*/
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `/api/trpc`,
}),
],
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
// queryClientConfig: { defaultOptions: { queries: { staleTime: 6000 } } },
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: false,
})(AppProviders);
28 changes: 28 additions & 0 deletions lib/trpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// ℹ️ Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import type { AppRouter } from "@server/routers/_app";
import { createReactQueryHooks } from "@trpc/react";
import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server";

/**
* A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
* @link https://trpc.io/docs/react#3-create-trpc-hooks
*/
export const trpc = createReactQueryHooks<AppRouter>();

// export const transformer = superjson;
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = inferQueryOutput<'hello'>
*/
export type inferQueryOutput<TRouteKey extends keyof AppRouter["_def"]["queries"]> = inferProcedureOutput<
AppRouter["_def"]["queries"][TRouteKey]
>;

export type inferQueryInput<TRouteKey extends keyof AppRouter["_def"]["queries"]> = inferProcedureInput<
AppRouter["_def"]["queries"][TRouteKey]
>;

export type inferMutationInput<TRouteKey extends keyof AppRouter["_def"]["mutations"]> = inferProcedureInput<
AppRouter["_def"]["mutations"][TRouteKey]
>;
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"@stripe/react-stripe-js": "^1.4.1",
"@stripe/stripe-js": "^1.16.0",
"@tailwindcss/forms": "^0.3.3",
"@trpc/client": "^9.8.0",
"@trpc/next": "^9.8.0",
"@trpc/react": "^9.8.0",
"@trpc/server": "^9.8.0",
"@types/stripe": "^8.0.417",
"accept-language-parser": "^1.5.0",
"async": "^3.2.1",
Expand Down Expand Up @@ -68,15 +72,16 @@
"react-intl": "^5.20.7",
"react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.1.25",
"react-query": "^3.21.0",
"react-query": "^3.23.1",
"react-select": "^4.3.1",
"react-timezone-select": "^1.0.7",
"react-use-intercom": "1.4.0",
"short-uuid": "^4.2.0",
"stripe": "^8.168.0",
"tsdav": "1.0.6",
"tslog": "^3.2.1",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"zod": "^3.8.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "2.0.4",
Expand Down
5 changes: 3 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export type AppProps = NextAppProps & {
err?: Error;
};

function MyApp({ Component, pageProps, err }: AppProps) {
function MyApp(props: AppProps) {
const { Component, pageProps, err } = props;
return (
<AppProviders pageProps={pageProps}>
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<Component {...pageProps} err={err} />
</AppProviders>
Expand Down
35 changes: 35 additions & 0 deletions pages/api/trpc/[trpc].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This file contains tRPC's HTTP response handler
*/
import { createContext } from "@server/createContext";
import { appRouter } from "@server/routers/_app";
import * as trpcNext from "@trpc/server/adapters/next";

export default trpcNext.createNextApiHandler({
router: appRouter,
/**
* @link https://trpc.io/docs/context
*/
createContext,
/**
* @link https://trpc.io/docs/error-handling
*/
onError({ error }) {
if (error.code === "INTERNAL_SERVER_ERROR") {
// send to bug reporting
console.error("Something went wrong", error);
}
},
/**
* Enable query batching
*/
batching: {
enabled: true,
},
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
// responseMeta() {
// // ...
// },
});
Loading

0 comments on commit 3430065

Please sign in to comment.