diff --git a/components/Shell.tsx b/components/Shell.tsx index 4670a9e9b0..ce5570a2ee 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -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 ; + } - return session ? ( + return ( <>
- +
@@ -206,19 +249,12 @@ export default function Shell(props) { - ) : null; + ); } function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean }) { - const [user, setUser] = useState(null); - - useEffect(() => { - fetch("/api/me") - .then((res) => res.json()) - .then((responseBody) => { - setUser(responseBody.user); - }); - }, []); + const query = useMeQuery(); + const user = query.data; return ( @@ -230,8 +266,8 @@ function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean }) {!small && ( - {user?.name} + {user.name} - /{user?.username} + /{user.username} )} diff --git a/components/ui/Avatar.tsx b/components/ui/Avatar.tsx index d345575cb6..8ace683809 100644 --- a/components/ui/Avatar.tsx +++ b/components/ui/Avatar.tsx @@ -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; 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 = ( diff --git a/cypress/integration/cancel.spec.ts b/cypress/integration/cancel.spec.ts index c442529e2e..31f3185e11 100644 --- a/cypress/integration/cancel.spec.ts +++ b/cypress/integration/cancel.spec.ts @@ -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(); diff --git a/lib/app-providers.tsx b/lib/app-providers.tsx index 496603b9f2..49ca1de46f 100644 --- a/lib/app-providers.tsx +++ b/lib/app-providers.tsx @@ -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 = ({ pageProps, children }) => { +const AppProviders = (props: AppProps) => { return ( - - - - - {children} - - - - + + + {props.children} + + ); }; -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); diff --git a/lib/trpc.ts b/lib/trpc.ts new file mode 100644 index 0000000000..12f74345a5 --- /dev/null +++ b/lib/trpc.ts @@ -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(); + +// 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 = inferProcedureOutput< + AppRouter["_def"]["queries"][TRouteKey] +>; + +export type inferQueryInput = inferProcedureInput< + AppRouter["_def"]["queries"][TRouteKey] +>; + +export type inferMutationInput = inferProcedureInput< + AppRouter["_def"]["mutations"][TRouteKey] +>; diff --git a/package.json b/package.json index 497b826b1b..b800a984cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/_app.tsx b/pages/_app.tsx index 99b0c1066d..88c67c5297 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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 ( - + diff --git a/pages/api/trpc/[trpc].ts b/pages/api/trpc/[trpc].ts new file mode 100644 index 0000000000..e0b9531d0e --- /dev/null +++ b/pages/api/trpc/[trpc].ts @@ -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() { + // // ... + // }, +}); diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx index 0e5eed3a1d..fcf56c4d14 100644 --- a/pages/auth/login.tsx +++ b/pages/auth/login.tsx @@ -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(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; } diff --git a/pages/availability/index.tsx b/pages/availability/index.tsx index 820affbd6d..568462b2f8 100644 --- a/pages/availability/index.tsx +++ b/pages/availability/index.tsx @@ -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(); const bufferMinsRef = useRef(); - if (loading) { + if (queryMe.status === "loading") { return ; } + if (queryMe.status !== "success") { + return ; + } + const user = queryMe.data; function toggleAddModal() { setShowAddModal(!showAddModal); @@ -126,8 +131,8 @@ export default function Availability(props) {

- 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)}.

@@ -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]} />
: @@ -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]} /> @@ -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]} /> : @@ -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]} /> @@ -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]} /> : @@ -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]} /> @@ -305,40 +310,3 @@ export default function Availability(props) { ); } - -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 }, - }; -} diff --git a/pages/bookings/index.tsx b/pages/bookings/index.tsx index 35d8b88f2a..0309e35ed8 100644 --- a/pages/bookings/index.tsx +++ b/pages/bookings/index.tsx @@ -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 ; - } - - 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 ( -
- -
-
-
- {isEmpty ? ( + +
+
+
+ {query.status === "error" && ( + + )} + {query.status === "loading" && } + {bookings && + (bookings.length === 0 ? (
- )} -
+ ))}
- -
+
+
); } - -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 } }; -} diff --git a/server/createContext.ts b/server/createContext.ts new file mode 100644 index 0000000000..44c526d65c --- /dev/null +++ b/server/createContext.ts @@ -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) { + 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; diff --git a/server/createRouter.ts b/server/createRouter.ts new file mode 100644 index 0000000000..26fc7303e9 --- /dev/null +++ b/server/createRouter.ts @@ -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(); +} diff --git a/server/routers/_app.ts b/server/routers/_app.ts new file mode 100644 index 0000000000..5809af5270 --- /dev/null +++ b/server/routers/_app.ts @@ -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; diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx new file mode 100644 index 0000000000..d980ddb0df --- /dev/null +++ b/server/routers/viewer.tsx @@ -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; + }, + }); diff --git a/server/ssg.ts b/server/ssg.ts new file mode 100644 index 0000000000..3c1025d260 --- /dev/null +++ b/server/ssg.ts @@ -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, + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 45f10e64a7..6fda3124ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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, diff --git a/yarn.lock b/yarn.lock index 833a9d3c9d..32678776a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==