* add trpc

* trpc specific

* fix deps

* lint fix

* upgrade prisma

* nativeTypes

* nope, not needed

* fix app propviders

* Revert "upgrade prisma"

This reverts commit e6f2d2542a.

* 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>
pull/615/head
Alex Johansson 2021-09-27 15:47:55 +01:00 committed by GitHub
parent 0938f6f4b2
commit 34300650e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 483 additions and 216 deletions

View File

@ -1,4 +1,3 @@
// TODO: replace headlessui with radix-ui
import { Menu, Transition } from "@headlessui/react";
import { SelectorIcon } from "@heroicons/react/outline";
import {
@ -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 = [
{
@ -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,
@ -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>
@ -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">
@ -230,8 +266,8 @@ 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"
@ -239,9 +275,9 @@ function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean })
/>
{!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>
)}

View File

@ -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)}
/>

View File

@ -1,10 +1,10 @@
describe("cancel", () => {
describe.skip("cancel", () => {
describe("Admin user can cancel events", () => {
before(() => {
cy.visit("/bookings");
cy.login("pro@example.com", "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();

View File

@ -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
lib/trpc.ts Normal file
View File

@ -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]
>;

View File

@ -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",
@ -68,7 +72,7 @@
"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",
@ -76,7 +80,8 @@
"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",

View File

@ -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>

35
pages/api/trpc/[trpc].ts Normal file
View File

@ -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() {
// // ...
// },
});

View File

@ -1,7 +1,7 @@
import { getCsrfToken, signIn } from "next-auth/client";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useState } from "react";
import { ErrorCode, getSession } from "@lib/auth";
@ -26,11 +26,7 @@ export default function Login({ csrfToken }) {
const [secondFactorRequired, setSecondFactorRequired] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
if (!router.query?.callbackUrl) {
window.history.replaceState(null, document.title, "?callbackUrl=/");
}
}, [router.query]);
const callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "/";
async function handleSubmit(e: React.SyntheticEvent) {
e.preventDefault();
@ -43,14 +39,20 @@ export default function Login({ csrfToken }) {
setErrorMessage(null);
try {
const response = await signIn("credentials", { redirect: false, email, password, totpCode: code });
const response = await signIn("credentials", {
redirect: false,
email,
password,
totpCode: code,
callbackUrl,
});
if (!response) {
console.error("Received empty response from next auth");
return;
}
if (!response.error) {
window.location.reload();
router.replace(callbackUrl);
return;
}

View File

@ -4,14 +4,15 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { getSession } from "@lib/auth";
import prisma from "@lib/prisma";
import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Modal from "@components/Modal";
import Shell from "@components/Shell";
import { Alert } from "@components/ui/Alert";
export default function Availability(props) {
export default function Availability() {
const queryMe = trpc.useQuery(["viewer.me"]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [session, loading] = useSession();
const router = useRouter();
@ -31,9 +32,13 @@ export default function Availability(props) {
const bufferHoursRef = useRef<HTMLInputElement>();
const bufferMinsRef = useRef<HTMLInputElement>();
if (loading) {
if (queryMe.status === "loading") {
return <Loader />;
}
if (queryMe.status !== "success") {
return <Alert severity="error" title="Something went wrong" />;
}
const user = queryMe.data;
function toggleAddModal() {
setShowAddModal(!showAddModal);
@ -126,8 +131,8 @@ export default function Availability(props) {
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end
at {convertMinsToHrsMins(props.user.endTime)}.
Currently, your day is set to start at {convertMinsToHrsMins(user.startTime)} and end at{" "}
{convertMinsToHrsMins(user.endTime)}.
</p>
</div>
<div className="mt-5">
@ -199,7 +204,7 @@ export default function Availability(props) {
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="9"
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[0]}
defaultValue={convertMinsToHrsMins(user.startTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
@ -214,7 +219,7 @@ export default function Availability(props) {
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[1]}
defaultValue={convertMinsToHrsMins(user.startTime).split(":")[1]}
/>
</div>
</div>
@ -231,7 +236,7 @@ export default function Availability(props) {
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="17"
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[0]}
defaultValue={convertMinsToHrsMins(user.endTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
@ -246,7 +251,7 @@ export default function Availability(props) {
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[1]}
defaultValue={convertMinsToHrsMins(user.endTime).split(":")[1]}
/>
</div>
</div>
@ -263,7 +268,7 @@ export default function Availability(props) {
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="0"
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[0]}
defaultValue={convertMinsToHrsMins(user.bufferTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
@ -278,7 +283,7 @@ export default function Availability(props) {
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="10"
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[1]}
defaultValue={convertMinsToHrsMins(user.bufferTime).split(":")[1]}
/>
</div>
</div>
@ -305,40 +310,3 @@ export default function Availability(props) {
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
startTime: true,
endTime: true,
bufferTime: true,
},
});
const types = await prisma.eventType.findMany({
where: {
userId: user.id,
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
},
});
return {
props: { session, user, types },
};
}

View File

@ -1,35 +1,28 @@
// TODO: replace headlessui with radix-ui
import { Menu, Transition } from "@headlessui/react";
import { ClockIcon, CalendarIcon, XIcon, CheckIcon, BanIcon } from "@heroicons/react/outline";
import { BanIcon, CalendarIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline";
import { DotsHorizontalIcon } from "@heroicons/react/solid";
import { BookingStatus, User } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
import { useSession } from "next-auth/client";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { getSession } from "@lib/auth";
import classNames from "@lib/classNames";
import prisma from "@lib/prisma";
import { trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader";
import Shell from "@components/Shell";
import { Alert } from "@components/ui/Alert";
import { Button } from "@components/ui/Button";
export default function Bookings({ bookings }) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [session, loading] = useSession();
const isEmpty = Object.keys(bookings).length === 0;
export default function Bookings() {
const query = trpc.useQuery(["viewer.bookings"]);
const bookings = query.data;
const router = useRouter();
if (loading) {
return <Loader />;
}
async function confirmBookingHandler(booking, confirm: boolean) {
async function confirmBookingHandler(booking: { id: number }, confirm: boolean) {
const res = await fetch("/api/book/confirm", {
method: "PATCH",
body: JSON.stringify({ id: booking.id, confirmed: confirm }),
@ -43,12 +36,16 @@ export default function Bookings({ bookings }) {
}
return (
<div>
<Shell heading="Bookings" subtitle="See upcoming and past events booked through your event type links.">
<div className="-mx-4 sm:mx-auto flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
{isEmpty ? (
<Shell heading="Bookings" subtitle="See upcoming and past events booked through your event type links.">
<div className="-mx-4 sm:mx-auto flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
{query.status === "error" && (
<Alert severity="error" title="Something went wrong" message={query.error.message} />
)}
{query.status === "loading" && <Loader />}
{bookings &&
(bookings.length === 0 ? (
<EmptyScreen
Icon={CalendarIcon}
headline="No upcoming bookings, yet"
@ -282,75 +279,10 @@ export default function Bookings({ bookings }) {
</tbody>
</table>
</div>
)}
</div>
))}
</div>
</div>
</Shell>
</div>
</div>
</Shell>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user: User = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
email: true,
},
});
const b = await prisma.booking.findMany({
where: {
OR: [
{
userId: session.user.id,
},
{
attendees: {
some: {
email: user.email,
},
},
},
],
},
select: {
uid: true,
title: true,
description: true,
attendees: true,
confirmed: true,
rejected: true,
id: true,
startTime: true,
endTime: true,
eventType: {
select: {
team: {
select: {
name: true,
},
},
},
},
status: true,
},
orderBy: {
startTime: "asc",
},
});
const bookings = b.reverse().map((booking) => {
return { ...booking, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString() };
});
return { props: { session, bookings } };
}

68
server/createContext.ts Normal file
View File

@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as trpc from "@trpc/server";
import { Maybe } from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import { getSession, Session } from "@lib/auth";
import prisma from "@lib/prisma";
import { defaultAvatarSrc } from "@lib/profile";
async function getUserFromSession(session: Maybe<Session>) {
if (!session?.user?.id) {
return null;
}
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
timeZone: true,
weekStart: true,
startTime: true,
endTime: true,
bufferTime: true,
theme: true,
createdDate: true,
hideBranding: true,
avatar: true,
},
});
// some hacks to make sure `username` and `email` are never inferred as `null`
if (!user) {
return null;
}
const { email, username } = user;
if (!username || !email) {
return null;
}
const avatar = user.avatar || defaultAvatarSrc({ email });
return {
...user,
avatar,
email,
username,
};
}
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = async ({ req, res }: trpcNext.CreateNextContextOptions) => {
// for API-response caching see https://trpc.io/docs/caching
const session = await getSession({ req });
return {
prisma,
session,
user: await getUserFromSession(session),
};
};
export type Context = trpc.inferAsyncReturnType<typeof createContext>;

10
server/createRouter.ts Normal file
View File

@ -0,0 +1,10 @@
import * as trpc from "@trpc/server";
import { Context } from "./createContext";
/**
* Helper function to create a router with context
*/
export function createRouter() {
return trpc.router<Context>();
}

26
server/routers/_app.ts Normal file
View File

@ -0,0 +1,26 @@
/**
* This file contains the root router of your tRPC-backend
*/
import { createRouter } from "../createRouter";
import { viewerRouter } from "./viewer";
/**
* Create your application's root router
* If you want to use SSG, you need export this
* @link https://trpc.io/docs/ssg
* @link https://trpc.io/docs/router
*/
export const appRouter = createRouter()
/**
* Add data transformers
* @link https://trpc.io/docs/data-transformers
*/
// .transformer(superjson)
/**
* Optionally do custom error (type safe!) formatting
* @link https://trpc.io/docs/error-formatting
*/
// .formatError(({ shape, error }) => { })
.merge("viewer.", viewerRouter);
export type AppRouter = typeof appRouter;

81
server/routers/viewer.tsx Normal file
View File

@ -0,0 +1,81 @@
import { TRPCError } from "@trpc/server";
import { createRouter } from "../createRouter";
// routes only available to authenticated users
export const viewerRouter = createRouter()
// check that user is authenticated
.middleware(({ ctx, next }) => {
const { user } = ctx;
if (!user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
...ctx,
// session value is known to be non-null now
user,
},
});
})
.query("me", {
resolve({ ctx }) {
return ctx.user;
},
})
.query("bookings", {
async resolve({ ctx }) {
const { prisma, user } = ctx;
const bookingsQuery = await prisma.booking.findMany({
where: {
OR: [
{
userId: user.id,
},
{
attendees: {
some: {
email: user.email,
},
},
},
],
},
select: {
uid: true,
title: true,
description: true,
attendees: true,
confirmed: true,
rejected: true,
id: true,
startTime: true,
endTime: true,
eventType: {
select: {
team: {
select: {
name: true,
},
},
},
},
status: true,
},
orderBy: {
startTime: "asc",
},
});
const bookings = bookingsQuery.reverse().map((booking) => {
return {
...booking,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
};
});
return bookings;
},
});

14
server/ssg.ts Normal file
View File

@ -0,0 +1,14 @@
import { createSSGHelpers } from "@trpc/react/ssg";
import prisma from "@lib/prisma";
import { appRouter } from "./routers/_app";
export const ssg = createSSGHelpers({
router: appRouter,
ctx: {
prisma,
session: null,
user: null,
},
});

View File

@ -6,11 +6,13 @@
"paths": {
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
"@ee/*": ["ee/*"]
},
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,

View File

@ -282,7 +282,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0":
version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz"
dependencies:
@ -1006,19 +1006,12 @@
"@radix-ui/react-primitive" "0.1.0"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-id@0.1.0":
"@radix-ui/react-id@0.1.0", "@radix-ui/react-id@^0.1.0":
version "0.1.0"
resolved "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.0.tgz"
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-id@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.0.tgz#d01067520fb8f4b09da3f914bfe6cb0f88c26721"
integrity sha512-SubMSz7rAtl6w8qZ9YBRbDe9GjW36JugBsc6aYqng8tFydvNtkuBMj86zN/x5QiomMo+r8ylBVvuWzRkS0WbBA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-label@0.1.0":
version "0.1.0"
resolved "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.0.tgz"
@ -1281,6 +1274,41 @@
javascript-natural-sort "0.7.1"
lodash "4.17.21"
"@trpc/client@^9.8.0":
version "9.8.0"
resolved "https://registry.npmjs.org/@trpc/client/-/client-9.8.0.tgz#e60f4ff1fff7c34b1f36e240441022192c463d6e"
integrity sha512-YGUJI8EvAykXKciDe62aLNwk4TNh4bX+qHSWtgbF5d6qRQOwViINpB6vR8YSugPvmC4F+hvKm61wSd01cCWN4g==
dependencies:
"@babel/runtime" "^7.9.0"
"@trpc/server" "^9.8.0"
"@trpc/next@^9.8.0":
version "9.8.0"
resolved "https://registry.npmjs.org/@trpc/next/-/next-9.8.0.tgz#ab76cf56de604551565f13509f9163fb98dc8b1a"
integrity sha512-VfpTPtFt8E2lgHVolFtE90DkP2mV8CqdvLQKQ8gY4OfsOoAlitnJPAVeAxvHPVruUyDUjuggFocc1TZnEWRdxg==
dependencies:
"@babel/runtime" "^7.9.0"
"@trpc/client" "^9.8.0"
"@trpc/react" "^9.8.0"
"@trpc/server" "^9.8.0"
react-ssr-prepass "^1.4.0"
"@trpc/react@^9.8.0":
version "9.8.0"
resolved "https://registry.npmjs.org/@trpc/react/-/react-9.8.0.tgz#1ae46b84da9fb4e257335e6bdb2a489d70a1a9b2"
integrity sha512-vErvC98QBQh0XzfPm9LA/dmGHBm6N9/m+B3XdIkimGOD055bsxgKMLW25BeeDV3gLWlzQJ7nwOrsxKRd6fLi3w==
dependencies:
"@babel/runtime" "^7.9.0"
"@trpc/client" "^9.8.0"
"@trpc/server" "^9.8.0"
"@trpc/server@^9.8.0":
version "9.8.0"
resolved "https://registry.npmjs.org/@trpc/server/-/server-9.8.0.tgz#f7e8a0ab46cc41179dc06722cb3dbe33901eddb6"
integrity sha512-YFmS+5SwDQ9NRO9JvNyl1oLprWE2AwS2huXYcGo9e3Fl5ju2Q2MN0JYvVh7XYNyp1rI2EKqVVRzXEuCNk+3vVQ==
dependencies:
tslib "^2.1.0"
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz"
@ -6004,10 +6032,10 @@ react-portal@^4.2.0:
dependencies:
prop-types "^15.5.8"
react-query@^3.21.0:
version "3.24.4"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.24.4.tgz"
integrity sha512-p/t18+FN5P//bk/xR39r4JRWEigYzia2+J3lmKWSZHYbcivQlygJixY+81NiTNxT1P+/P6cl173b1lEbh1R8yQ==
react-query@^3.23.1:
version "3.23.1"
resolved "https://registry.npmjs.org/react-query/-/react-query-3.23.1.tgz#cde2d268958716d34a23e62aabba668752ba8f95"
integrity sha512-pq0vEwB5PNGvkWJNUk0qPpsxcDmhzY80ZLNPLIVQJ3k2UyXoGccPTrgOIj4Kz2TrMfgvRBTNwiSxHdaW7Sl0WQ==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
@ -6046,6 +6074,11 @@ react-select@^4.3.1:
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0"
react-ssr-prepass@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.4.0.tgz#33a3db19414f0f8f9f3f781c88f760ae366b4f51"
integrity sha512-0SzdmiQUtHvhxCabHg9BI/pkJfijGkQ0jQL6fC4YFy7idaDOuaiQLsajIkkNxffFXtJFHIWFITlve2WB88e0Jw==
react-style-singleton@^2.1.0:
version "2.1.1"
resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz"
@ -7421,3 +7454,8 @@ zen-observable-ts@^1.0.0:
zen-observable@0.8.15:
version "0.8.15"
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
zod@^3.8.2:
version "3.8.2"
resolved "https://registry.npmjs.org/zod/-/zod-3.8.2.tgz#f25b78bc76e64f31318d242e301c23d3d610b7a1"
integrity sha512-kpwVRACazsOhELVt5h4R2pC2OndrqaBK4+z134TWOsnzn7n2uOYnSyvx0QAn410pl28CgVtkSi5ew7e/AgO0oA==