diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 36862b5ea..f61c290c8 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -33,15 +33,17 @@ import { theme } from '../tools/server/theme/theme'; import { useEditModeInformationStore } from '../hooks/useEditModeInformation'; import '../styles/global.scss'; +import nextI18nextConfig from '../../next-i18next.config'; +import { api } from '~/utils/api'; function App( this: any, - props: AppProps & { + props: AppProps<{ colorScheme: ColorScheme; packageAttributes: ServerSidePackageAttributesType; editModeEnabled: boolean; defaultColorScheme: ColorScheme; - } + }> ) { const { Component, pageProps } = props; const [primaryColor, setPrimaryColor] = useState('red'); @@ -58,7 +60,7 @@ function App( // hook will return either 'dark' or 'light' on client // and always 'light' during ssr as window.matchMedia is not available - const preferredColorScheme = useColorScheme(props.defaultColorScheme); + const preferredColorScheme = useColorScheme(props.pageProps.defaultColorScheme); const [colorScheme, setColorScheme] = useLocalStorage({ key: 'mantine-color-scheme', defaultValue: preferredColorScheme, @@ -69,9 +71,9 @@ function App( const { setDisabled } = useEditModeInformationStore(); useEffect(() => { - setInitialPackageAttributes(props.packageAttributes); + setInitialPackageAttributes(props.pageProps.packageAttributes); - if (!props.editModeEnabled) { + if (!props.pageProps.editModeEnabled) { setDisabled(); } }, []); @@ -161,11 +163,13 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => { const colorScheme: ColorScheme = (process.env.DEFAULT_COLOR_SCHEME as ColorScheme) ?? 'light'; return { - colorScheme: getCookie('color-scheme', ctx) || 'light', - packageAttributes: getServiceSidePackageAttributes(), - editModeEnabled: !disableEditMode, - defaultColorScheme: colorScheme, + pageProps: { + colorScheme: getCookie('color-scheme', ctx) || 'light', + packageAttributes: getServiceSidePackageAttributes(), + editModeEnabled: !disableEditMode, + defaultColorScheme: colorScheme, + }, }; }; -export default appWithTranslation(App); +export default appWithTranslation(api.withTRPC(App), nextI18nextConfig as any); diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts new file mode 100644 index 000000000..67c9b4899 --- /dev/null +++ b/src/pages/api/trpc/[trpc].ts @@ -0,0 +1,16 @@ +import { createNextApiHandler } from '@trpc/server/adapters/next'; +import Consola from 'consola'; +import { createTRPCContext } from '~/server/api/trpc'; +import { appRouter } from '~/server/api/root'; + +// export API handler +export default createNextApiHandler({ + router: appRouter, + createContext: createTRPCContext, + onError: + process.env.NODE_ENV === 'development' + ? ({ path, error }) => { + Consola.error(`❌ tRPC failed on ${path ?? ''}: ${error.message}`); + } + : undefined, +}); diff --git a/src/server/api/root.ts b/src/server/api/root.ts new file mode 100644 index 000000000..57f7f9f98 --- /dev/null +++ b/src/server/api/root.ts @@ -0,0 +1,11 @@ +import { createTRPCRouter } from '~/server/api/trpc'; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const rootRouter = createTRPCRouter({}); + +// export type definition of API +export type RootRouter = typeof rootRouter; diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts new file mode 100644 index 000000000..748edda91 --- /dev/null +++ b/src/server/api/trpc.ts @@ -0,0 +1,93 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC } from '@trpc/server'; +import { type CreateNextContextOptions } from '@trpc/server/adapters/next'; +import superjson from 'superjson'; +import { ZodError } from 'zod'; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +type CreateContextOptions = Record; + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +const createInnerTRPCContext = (opts: CreateContextOptions) => ({}); + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: CreateNextContextOptions) => { + const { req, res } = opts; + + // Get the session from the server using the getServerSession wrapper function + + return createInnerTRPCContext({}); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 000000000..9181abda0 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,68 @@ +/** + * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which + * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. + * + * We also create a few inference helpers for input and output types. + */ +import { httpBatchLink, loggerLink } from '@trpc/client'; +import { createTRPCNext } from '@trpc/next'; +import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; +import superjson from 'superjson'; + +import { type RootRouter } from '~/server/api/root'; + +const getBaseUrl = () => { + if (typeof window !== 'undefined') return ''; // browser should use relative url + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url + return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost +}; + +/** A set of type-safe react-query hooks for your tRPC API. */ +export const api = createTRPCNext({ + config() { + return { + /** + * Transformer used for data de-serialization from the server. + * + * @see https://trpc.io/docs/data-transformers + */ + transformer: superjson, + + /** + * Links used to determine request flow from client to server. + * + * @see https://trpc.io/docs/links + */ + links: [ + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === 'development' || + (opts.direction === 'down' && opts.result instanceof Error), + }), + httpBatchLink({ + url: `${getBaseUrl()}/api/trpc`, + }), + ], + }; + }, + /** + * Whether tRPC should await queries when server rendering pages. + * + * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false + */ + ssr: false, +}); + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs;