├── sandbox.config.json
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── main.yml
│ └── codeql-analysis.yml
├── public
├── favicon.ico
└── vercel.svg
├── src
├── server
│ ├── lib
│ │ ├── sum.ts
│ │ └── sum.test.ts
│ └── trpc
│ │ ├── routers
│ │ ├── source.ts
│ │ └── index.ts
│ │ ├── trpc.ts
│ │ └── context.ts
├── pages
│ ├── next-auth.tsx
│ ├── react-hook-form.tsx
│ ├── api
│ │ ├── trpc
│ │ │ └── [trpc].ts
│ │ └── auth
│ │ │ └── [...nextauth].ts
│ ├── ssg.tsx
│ ├── index.tsx
│ └── _app.tsx
├── feature
│ ├── ssg
│ │ ├── meta.tsx
│ │ └── router.ts
│ ├── react-hook-form
│ │ ├── router.ts
│ │ ├── meta.tsx
│ │ ├── Form.tsx
│ │ └── index.tsx
│ └── next-auth
│ │ ├── router.tsx
│ │ ├── meta.tsx
│ │ └── index.tsx
├── utils
│ ├── useClipboard.ts
│ ├── ClientSuspense.tsx
│ ├── trpc.ts
│ └── ExamplePage.tsx
└── styles
│ └── globals.css
├── postcss.config.js
├── .vscode
├── settings.json
└── extensions.json
├── next.config.js
├── README.md
├── .env-example
├── next-env.d.ts
├── docker-compose.yaml
├── .kodiak.toml
├── tailwind.config.js
├── .gitignore
├── tsconfig.json
├── jest.config.ts
├── .eslintrc
└── package.json
/sandbox.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "next"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: KATT
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trpc/examples-kitchen-sink/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/server/lib/sum.ts:
--------------------------------------------------------------------------------
1 | export function sum(a: number, b: number) {
2 | return a + b;
3 | }
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/server/lib/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from './sum';
2 |
3 | test('sum', () => {
4 | expect(sum(1, 1)).toBe(2);
5 | });
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.fixAll.eslint": true
4 | },
5 | "typescript.tsdk": "node_modules/typescript/lib"
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @link https://nextjs.org/docs/api-reference/next.config.js/introduction
3 | */
4 | module.exports = {
5 | reactStrictMode: true,
6 | };
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tRPC Kitchen Sink
2 |
3 | https://kitchen-sink.trpc.io/
4 |
5 |
6 | ## Wanna Contribute? 🙏
7 |
8 | See [#1254](https://github.com/trpc/trpc/issues/1254)
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "esbenp.prettier-vscode",
4 | "dbaeumer.vscode-eslint",
5 | "Orta.vscode-twoslash-queries"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.env-example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgresql://postgres:@localhost:5792/trpcdb
2 | NEXTAUTH_URL=http://localhost:3000
3 | NEXTAUTH_SECRET=changeme
4 | NEXT_PUBLIC_APP_URL=http://localhost:3000
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 2
8 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/pages/next-auth.tsx:
--------------------------------------------------------------------------------
1 | import FeaturePage from 'feature/next-auth';
2 | import { meta } from 'feature/next-auth/meta';
3 | import { ExamplePage } from 'utils/ExamplePage';
4 |
5 | export default function Page() {
6 | return (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/react-hook-form.tsx:
--------------------------------------------------------------------------------
1 | import FeaturePage from 'feature/react-hook-form';
2 | import { meta } from 'feature/react-hook-form/meta';
3 | import { ExamplePage } from 'utils/ExamplePage';
4 |
5 | export default function Page() {
6 | return (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.6'
2 | services:
3 | postgres:
4 | image: postgres:13
5 | ports:
6 | - '5792:5432' # expose pg on port 5632 to not collide with pg from elswhere
7 | restart: always
8 | volumes:
9 | - db_data:/var/lib/postgresql/data
10 | environment:
11 | POSTGRES_PASSWORD: ${PGPASSWORD}
12 | POSTGRES_HOST_AUTH_METHOD: trust
13 | volumes:
14 | db_data:
15 |
--------------------------------------------------------------------------------
/.kodiak.toml:
--------------------------------------------------------------------------------
1 | # .kodiak.toml
2 | version = 1
3 |
4 | [approve]
5 | auto_approve_usernames = ["dependabot"]
6 |
7 | [merge]
8 | method = "squash"
9 | automerge_label = ["🚀 merge", "⬆️ dependencies"]
10 |
11 | [merge.automerge_dependencies]
12 | # only auto merge "minor" and "patch" version upgrades.
13 | versions = ["minor", "patch"]
14 | usernames = ["dependabot"]
15 |
16 | [update]
17 | autoupdate_label = "♻️ autoupdate"
18 |
--------------------------------------------------------------------------------
/src/feature/ssg/meta.tsx:
--------------------------------------------------------------------------------
1 | import { ExampleProps } from 'utils/ExamplePage';
2 |
3 | export const meta: ExampleProps = {
4 | title: 'Static Site Generation (SSG)',
5 | href: '/ssg',
6 | summary: (
7 | <>
8 |
9 | Using Static Site Generation & getStaticProps
10 |
11 | >
12 | ),
13 | files: [
14 | { title: 'Router', path: 'feature/ssg/router.ts' },
15 | { title: 'Page', path: 'pages/ssg.tsx' },
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/src/feature/ssg/router.ts:
--------------------------------------------------------------------------------
1 | import { t } from 'server/trpc/trpc';
2 | import { z } from 'zod';
3 |
4 | const posts = [
5 | {
6 | id: '1',
7 | title: 'This data comes from the backend',
8 | },
9 | ];
10 |
11 | const ssgRouter = t.router({
12 | byId: t.procedure
13 | .input(
14 | z.object({
15 | id: z.string(),
16 | }),
17 | )
18 | .query(async ({ input }) => {
19 | const post = posts.find((post) => post.id === input.id);
20 | return post ?? null;
21 | }),
22 | });
23 |
24 | export default ssgRouter;
25 |
--------------------------------------------------------------------------------
/src/utils/useClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useClipboard = (text: string): [boolean, () => void] => {
4 | const [hasCopied, setHasCopied] = useState(false);
5 |
6 | useEffect(() => {
7 | if (hasCopied) {
8 | const id = setTimeout(() => {
9 | setHasCopied(false);
10 | }, 600);
11 |
12 | return () => {
13 | clearTimeout(id);
14 | };
15 | }
16 | }, [hasCopied]);
17 |
18 | return [
19 | hasCopied,
20 | () => {
21 | navigator.clipboard.writeText(text).then(() => setHasCopied(true));
22 | },
23 | ];
24 | };
25 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{js,ts,jsx,tsx}'],
3 | theme: {
4 | extend: {
5 | colors: {
6 | primary: {
7 | 100: '#e8f0f9',
8 | 200: '#bbd3ee',
9 | 300: '#8db6e3',
10 | 400: '#337ccc',
11 | 500: '#3178c6',
12 | 600: '#27609f',
13 | 700: '#1c4572',
14 | 800: '#112944',
15 | 900: '#060e17',
16 | },
17 | },
18 | },
19 | },
20 | variants: {
21 | extend: {},
22 | },
23 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
24 | };
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | *.db
38 | *.db-journal
39 |
40 |
41 | # testing
42 | playwright/videos
43 | playwright/screenshots
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "strictNullChecks": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "types": ["@types/jest"],
18 | "baseUrl": "./src",
19 | "incremental": true
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/server/trpc/routers/source.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { z } from 'zod';
4 |
5 | import { t } from '../trpc';
6 |
7 | export const sourceRouter = t.router({
8 | getSource: t.procedure
9 | .input(
10 | z.object({
11 | path: z.string().refine((val) => !val.includes('..'), {
12 | message: 'Only relative paths allowed',
13 | }),
14 | }),
15 | )
16 | .query(async ({ input }) => {
17 | const ROOT = path.resolve(__dirname + '/../../../../../src') + '/';
18 | const contents = fs.readFileSync(ROOT + input.path).toString('utf8');
19 |
20 | return {
21 | contents,
22 | };
23 | }),
24 | });
25 |
--------------------------------------------------------------------------------
/src/feature/react-hook-form/router.ts:
--------------------------------------------------------------------------------
1 | import { t } from 'server/trpc/trpc';
2 |
3 | import { validationSchema } from './index';
4 |
5 | const items = [
6 | {
7 | id: '1',
8 | title: 'Hello tRPC',
9 | text: 'Hello world',
10 | },
11 | ];
12 |
13 | export const reactHookFormRouter = t.router({
14 | list: t.procedure.query(async () => {
15 | return items;
16 | }),
17 |
18 | add: t.procedure.input(validationSchema).mutation(({ input }) => {
19 | const id = Math.random()
20 | .toString(36)
21 | .replace(/[^a-z]+/g, '')
22 | .slice(0, 6);
23 | const item = {
24 | id,
25 | ...input,
26 | };
27 | items.push(item);
28 |
29 | return item;
30 | }),
31 | });
32 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | verbose: true,
5 | roots: [''],
6 | testMatch: [
7 | '**/tests/**/*.+(ts|tsx|js)',
8 | '**/?(*.)+(spec|test).+(ts|tsx|js)',
9 | ],
10 | testPathIgnorePatterns: ['/.next', '/playwright/'],
11 | transform: {
12 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
13 | },
14 | transformIgnorePatterns: [
15 | '/node_modules/',
16 | '^.+\\.module\\.(css|sass|scss)$',
17 | ],
18 | testEnvironment: 'jsdom',
19 | moduleNameMapper: {
20 | '^@components(.*)$': '/components$1',
21 | '^@lib(.*)$': '/lib$1',
22 | },
23 | };
24 |
25 | export default config;
26 |
--------------------------------------------------------------------------------
/src/server/trpc/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from '@trpc/server';
2 | import superjson from 'superjson';
3 |
4 | import type { Context } from './context';
5 |
6 | export const t = initTRPC.context().create({
7 | /**
8 | * SuperJSON allows us to transparently use e.g. standard Date/Map/Sets over the wire
9 | * between the server and client. That means you can return any of these types in your
10 | * API-resolver and use them in the client without recreating the objects from JSON.
11 | * https://trpc.io/docs/v10/data-transformers#using-superjson
12 | */
13 | transformer: superjson,
14 | /**
15 | * Optionally do custom error (type safe!) formatting
16 | * https://trpc.io/docs/v10/error-formatting
17 | */
18 | errorFormatter({ shape }) {
19 | return shape;
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/utils/ClientSuspense.tsx:
--------------------------------------------------------------------------------
1 | import { Component, ErrorInfo, ReactNode } from 'react';
2 |
3 | interface Props {
4 | children: ReactNode;
5 | }
6 |
7 | interface State {
8 | hasError: boolean;
9 | }
10 |
11 | export class ErrorBoundary extends Component {
12 | public state: State = {
13 | hasError: false,
14 | };
15 |
16 | public static getDerivedStateFromError(): State {
17 | // Update state so the next render will show the fallback UI.
18 | return { hasError: true };
19 | }
20 |
21 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
22 | console.error('Uncaught error:', error, errorInfo);
23 | }
24 |
25 | public render() {
26 | if (this.state.hasError) {
27 | return Sorry.. there was an error ;
28 | }
29 |
30 | return this.props.children;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | scroll-behavior: smooth;
7 | }
8 | body {
9 | @apply bg-primary-100;
10 | }
11 |
12 | .btn {
13 | @apply relative inline-flex items-center px-3 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 transition ease-in-out duration-150 rounded-md;
14 | }
15 | .btn__icon {
16 | @apply mr-2 h-5 w-5 transition-colors opacity-90;
17 | }
18 |
19 | .btn--active {
20 | @apply bg-primary-500 text-white;
21 | }
22 |
23 | .btn:focus {
24 | @apply z-10 outline-none border-primary-300;
25 | }
26 |
27 | .btn-group {
28 | @apply relative z-0 inline-flex shadow-sm;
29 | }
30 |
31 | .btn-group > .btn:not(:first-child) {
32 | @apply rounded-l-none -ml-px;
33 | }
34 |
35 | .btn-group > .btn:not(:last-child) {
36 | @apply rounded-r-none;
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains tRPC's HTTP response handler
3 | */
4 | import * as trpcNext from '@trpc/server/adapters/next';
5 | import { createContext } from 'server/trpc/context';
6 | import { appRouter } from 'server/trpc/routers';
7 |
8 | export default trpcNext.createNextApiHandler({
9 | router: appRouter,
10 | /**
11 | * @link https://trpc.io/docs/context
12 | */
13 | createContext,
14 | /**
15 | * @link https://trpc.io/docs/error-handling
16 | */
17 | onError({ error }) {
18 | if (error.code === 'INTERNAL_SERVER_ERROR') {
19 | // send to bug reporting
20 | console.error('Something went wrong', error);
21 | }
22 | },
23 | /**
24 | * Enable query batching
25 | */
26 | batching: {
27 | enabled: true,
28 | },
29 | /**
30 | * @link https://trpc.io/docs/caching#api-response-caching
31 | */
32 | // responseMeta() {
33 | // // ...
34 | // },
35 | });
36 |
--------------------------------------------------------------------------------
/src/server/trpc/routers/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains the root router of your tRPC-backend
3 | */
4 | import { authRouter } from 'feature/next-auth/router';
5 | import { reactHookFormRouter } from 'feature/react-hook-form/router';
6 | import ssgRouter from 'feature/ssg/router';
7 |
8 | import { t } from '../trpc';
9 | import { sourceRouter } from './source';
10 |
11 | /**
12 | * In tRPC v10 the root router is created by the same function as child
13 | * routers, and they can be nested arbitrarily.
14 | * The root router gets passed to `createNextApiHandler` to handle routing in /api/trpc
15 | * The root router's type gets passed to `createTRPCNext` so the frontend knows the routes/schema/returns
16 | */
17 | export const appRouter = t.router({
18 | healthcheck: t.procedure.query(() => 'ok'),
19 |
20 | source: sourceRouter,
21 | ssgRouter: ssgRouter,
22 | authRouter: authRouter,
23 | reactHookFormRouter: reactHookFormRouter,
24 | });
25 | export type AppRouter = typeof appRouter;
26 |
--------------------------------------------------------------------------------
/src/feature/next-auth/router.tsx:
--------------------------------------------------------------------------------
1 | import { TRPCError } from '@trpc/server';
2 | import { t } from 'server/trpc/trpc';
3 |
4 | const authMiddleware = t.middleware(async ({ ctx, next }) => {
5 | // any query that uses this middleware will throw
6 | // an error unless there is a current session
7 | if (!ctx.session) {
8 | throw new TRPCError({ code: 'UNAUTHORIZED' });
9 | }
10 | return next();
11 | });
12 |
13 | // you can create a named procedure that uses a middleware
14 | // (as is done here),
15 | // or just use the middleware inline in the router like:
16 | // `someProcedure: t.procedure.use(someMiddleware).query()
17 | const authedProcedure = t.procedure.use(authMiddleware);
18 |
19 | export const authRouter = t.router({
20 | getSession: t.procedure.query(({ ctx }) => {
21 | // The session object is added to the routers context
22 | // in the context file server side
23 | return ctx.session;
24 | }),
25 | getSecretCode: authedProcedure.query(async () => {
26 | const secretCode = 'the cake is a lie.';
27 | return secretCode;
28 | }),
29 | });
30 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/src/feature/next-auth/meta.tsx:
--------------------------------------------------------------------------------
1 | import { ExampleProps } from 'utils/ExamplePage';
2 |
3 | export const meta: ExampleProps = {
4 | title: 'Next-Auth Authentication',
5 | href: '/next-auth',
6 | // This is only enabled on the client as it will cause hydration error otherwise
7 | // The problem is that RSC won't send the right header whilst the client will, leading to inconsistent behavior
8 | clientOnly: true,
9 | summary: (
10 | <>
11 |
12 | Using tRPC & NextAuth
13 |
14 | >
15 | ),
16 | detail: (
17 | <>
18 |
19 | Using tRPC &{' '}
20 |
26 | NextAuth
27 |
28 | .
29 |
30 | >
31 | ),
32 | files: [
33 | { title: 'Page', path: 'feature/next-auth/index.tsx' },
34 | { title: 'Router', path: 'feature/next-auth/router.tsx' },
35 | { title: 'Context', path: 'server/trpc/context.ts' },
36 | ],
37 | };
38 |
--------------------------------------------------------------------------------
/src/feature/react-hook-form/meta.tsx:
--------------------------------------------------------------------------------
1 | import { ExampleProps } from 'utils/ExamplePage';
2 |
3 | export const meta: ExampleProps = {
4 | title: 'React Hook Form',
5 | href: '/react-hook-form',
6 | clientOnly: true,
7 | summary: (
8 | <>
9 |
10 | Using tRPC with react-hook-form.
11 |
12 | >
13 | ),
14 | detail: (
15 | <>
16 |
17 | Using tRPC &{' '}
18 |
24 | react-hook-form.
25 |
26 |
27 | Note how the same
28 | zod
29 | {' '}
30 | validation schema is reused both for the client & the server.
31 |
32 | >
33 | ),
34 | files: [
35 | { title: 'Router', path: 'feature/react-hook-form/router.ts' },
36 | { title: 'Page', path: 'feature/react-hook-form/index.tsx' },
37 | { title: 'Form utils', path: 'feature/react-hook-form/Form.tsx' },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/src/server/trpc/context.ts:
--------------------------------------------------------------------------------
1 | import * as trpc from '@trpc/server';
2 | import * as trpcNext from '@trpc/server/adapters/next';
3 | import { unstable_getServerSession } from 'next-auth';
4 |
5 | import { authOptions as nextAuthOptions } from '../../pages/api/auth/[...nextauth]';
6 |
7 | /**
8 | * Creates context for an incoming request
9 | * @link https://trpc.io/docs/context
10 | */
11 | export const createContext = async (
12 | opts?: trpcNext.CreateNextContextOptions,
13 | ) => {
14 | const req = opts?.req;
15 | const res = opts?.res;
16 |
17 | /**
18 | * Uses faster "unstable_getServerSession" in next-auth v4 that avoids
19 | * a fetch request to /api/auth.
20 | * This function also updates the session cookie whereas getSession does not
21 | * Note: If no req -> SSG is being used -> no session exists (null)
22 | * @link https://github.com/nextauthjs/next-auth/issues/1535
23 | */
24 | // const session = opts && (await getServerSession(opts, nextAuthOptions));
25 | const session =
26 | req && res && (await unstable_getServerSession(req, res, nextAuthOptions));
27 |
28 | // for API-response caching see https://trpc.io/docs/caching
29 | return {
30 | req,
31 | res,
32 | session,
33 | };
34 | };
35 |
36 | export type Context = trpc.inferAsyncReturnType;
37 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser", // Specifies the ESLint parser
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
5 | "plugin:react/recommended",
6 | "plugin:react-hooks/recommended",
7 | "plugin:prettier/recommended"
8 | ],
9 | "parserOptions": {
10 | "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
11 | "sourceType": "module" // Allows for the use of imports
12 | },
13 | "plugins": ["simple-import-sort"],
14 | "rules": {
15 | // Auto-sort imports and exports
16 | "simple-import-sort/imports": "error",
17 | "simple-import-sort/exports": "error",
18 | "sort-imports": "off",
19 | "import/order": "off",
20 |
21 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
22 | "@typescript-eslint/explicit-function-return-type": "off",
23 | "@typescript-eslint/explicit-module-boundary-types": "off",
24 | "react/react-in-jsx-scope": "off",
25 | "react/prop-types": "off",
26 | "@typescript-eslint/no-explicit-any": "off"
27 | },
28 | // "overrides": [
29 | // {
30 | // "files": [],
31 | // "rules": {
32 | // "@typescript-eslint/no-unused-vars": "off"
33 | // }
34 | // }
35 | // ],
36 | "settings": {
37 | "react": {
38 | "version": "detect"
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/ssg.tsx:
--------------------------------------------------------------------------------
1 | import { createServerSideHelpers } from '@trpc/react-query/server';
2 | import { meta } from 'feature/ssg/meta';
3 | import { InferGetStaticPropsType } from 'next';
4 | import { createContext } from 'server/trpc/context';
5 | import { appRouter } from 'server/trpc/routers';
6 | import superjson from 'superjson';
7 | import { ExamplePage } from 'utils/ExamplePage';
8 | import { trpc } from 'utils/trpc';
9 |
10 | export default function Page(
11 | props: InferGetStaticPropsType,
12 | ) {
13 | const { id } = props;
14 | const query = trpc.ssgRouter.byId.useQuery({ id });
15 |
16 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
17 | const post = query.data!;
18 |
19 | return (
20 | <>
21 |
22 |
23 | {post.title}
24 |
25 |
26 | >
27 | );
28 | }
29 |
30 | export async function getStaticProps() {
31 | const ssgHelper = createServerSideHelpers({
32 | router: appRouter,
33 | ctx: await createContext(),
34 | transformer: superjson, // optional - adds superjson serialization
35 | });
36 |
37 | const id = '1';
38 | const post = await ssgHelper.ssgRouter.byId.fetch({ id });
39 |
40 | if (!post) {
41 | return {
42 | notFound: true,
43 | };
44 | }
45 | return {
46 | props: {
47 | trpcState: ssgHelper.dehydrate(),
48 | id,
49 | },
50 | revalidate: 1,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: E2E-testing
2 | on: [push]
3 | jobs:
4 | e2e:
5 | env:
6 | DATABASE_URL: postgresql://postgres:@localhost:5432/trpcdb
7 | NEXTAUTH_SECRET: test
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | matrix:
11 | node: ['14.x']
12 | os: [ubuntu-latest]
13 | services:
14 | postgres:
15 | image: postgres:12.1
16 | env:
17 | POSTGRES_USER: postgres
18 | POSTGRES_DB: trpcdb
19 | ports:
20 | - 5432:5432
21 | steps:
22 | - name: Checkout repo
23 | uses: actions/checkout@v2
24 |
25 | - uses: pnpm/action-setup@v2.2.4
26 |
27 | - name: Get pnpm store directory
28 | id: pnpm-cache
29 | run: |
30 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
31 |
32 | - uses: actions/cache@v3
33 | name: Setup pnpm cache
34 | with:
35 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
36 | key: ${{ runner.os }}-pnpm-store-${{ matrix.dir }}-${{ hashFiles('**/pnpm-lock.yaml') }}
37 | restore-keys: |
38 | ${{ runner.os }}-pnpm-store-${{ matrix.dir }}-
39 |
40 | - run: node -v
41 | - name: Install deps (with cache)
42 | run: pnpm install
43 |
44 | - name: Next.js cache
45 | uses: actions/cache@v2
46 | with:
47 | path: ${{ github.workspace }}/.next/cache
48 | key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/pnpm.lock') }}-nextjs
49 |
50 | - run: pnpm lint
51 | - run: pnpm build
52 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { NextAuthOptions, User } from 'next-auth';
2 | import CredentialsProvider from 'next-auth/providers/credentials';
3 |
4 | export const authOptions: NextAuthOptions = {
5 | providers: [
6 | CredentialsProvider({
7 | // The name to display on the sign in form (e.g. 'Sign in with...')
8 | name: 'Next Auth',
9 | // The credentials is used to generate a suitable form on the sign in page.
10 | // You can specify whatever fields you are expecting to be submitted.
11 | // e.g. domain, username, password, 2FA token, etc.
12 | // You can pass any HTML attribute to the tag through the object.
13 | credentials: {
14 | username: {
15 | label: 'Username',
16 | type: 'text',
17 | placeholder: 'Any credentials work',
18 | },
19 | password: { label: 'Password', type: 'password' },
20 | },
21 | async authorize(credentials) {
22 | // You need to provide your own logic here that takes the credentials
23 | // submitted and returns either a object representing a user or value
24 | // that is false/null if the credentials are invalid.
25 | // e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
26 | // You can also use the `req` object to obtain additional parameters
27 | // (i.e., the request IP address)
28 |
29 | const user: User = {
30 | id: '1',
31 | name: 'J Smith',
32 | email: credentials?.username,
33 | };
34 |
35 | return user;
36 | },
37 | }),
38 | ],
39 | secret: process.env.NEXTAUTH_SECRET,
40 | };
41 |
42 | export default NextAuth(authOptions);
43 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { meta as nextAuthMeta } from 'feature/next-auth/meta';
2 | import { meta as reactHookFormMeta } from 'feature/react-hook-form/meta';
3 | import { meta as ssgMeta } from 'feature/ssg/meta';
4 | import Head from 'next/head';
5 | import Link from 'next/link';
6 | import { ExampleProps } from 'utils/ExamplePage';
7 |
8 | const propsList: ExampleProps[] = [reactHookFormMeta, ssgMeta, nextAuthMeta];
9 |
10 | export default function Page() {
11 | return (
12 | <>
13 |
14 | tRPC Kitchen Sink
15 |
16 |
17 |
18 |
19 | A collection tRPC usage patterns
20 |
21 |
22 | Your go-to place to find out how to find solutions to common
23 | problems.
24 |
25 |
26 |
27 |
28 |
40 |
41 | >
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 |
3 | import { AppType } from 'next/dist/shared/lib/utils';
4 | import { useEffect, useState } from 'react';
5 | import { trpc } from 'utils/trpc';
6 |
7 | function ContributorsWantedBanner() {
8 | const [visible, setVisible] = useState(false);
9 | useEffect(() => {
10 | setTimeout(() => {
11 | setVisible(true);
12 | }, 3e3);
13 | }, []);
14 |
15 | return (
16 | <>
17 |
18 |
46 | >
47 | );
48 | }
49 |
50 | const MyApp: AppType = (props) => {
51 | return (
52 | <>
53 |
54 |
55 | >
56 | );
57 | };
58 |
59 | export default trpc.withTRPC(MyApp);
60 |
--------------------------------------------------------------------------------
/src/feature/next-auth/index.tsx:
--------------------------------------------------------------------------------
1 | import { signIn, signOut } from 'next-auth/react';
2 | import { trpc } from 'utils/trpc';
3 |
4 | export default function NextAuth() {
5 | return (
6 | <>
7 | Next Auth Examples
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | function ServerSideSessionCheck() {
16 | const [session] = trpc.authRouter.getSession.useSuspenseQuery();
17 |
18 | return (
19 |
20 |
21 | Server side session check with tRPC's context
22 |
23 | {session ? (
24 | <>
25 | Signed in as {session?.user?.email}
26 | >
27 | ) : (
28 | <>
29 | Not signed in
30 | >
31 | )}
32 |
33 | );
34 | }
35 |
36 | function MiddlewareQuery() {
37 | const query = trpc.authRouter.getSecretCode.useQuery();
38 |
39 | const secretCode = query.data;
40 | return (
41 |
42 |
43 | Server side middleware session check with tRPC's context
44 |
45 | {secretCode ? (
46 | <>
47 | You're logged in. The secret code from the server is: "
48 | {secretCode}"
49 |
50 | >
51 | ) : (
52 | <>
53 | Not signed in, no code from the server, a 401 response and an error is
54 | raised.
55 | >
56 | )}
57 |
58 | );
59 | }
60 |
61 | function SignInButton() {
62 | const [session] = trpc.authRouter.getSession.useSuspenseQuery();
63 |
64 | return (
65 |
66 |
{
71 | signOut();
72 | }
73 | : () => {
74 | signIn();
75 | }
76 | }
77 | >
78 | {session ? 'Sign Out' : 'Sign In'}
79 |
80 |
(Any credentials work)
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@examples/kitchen-sink",
3 | "version": "9.14.0",
4 | "private": true,
5 | "packageManager": "pnpm@7.18.2",
6 | "scripts": {
7 | "build": "next build",
8 | "dev": "next dev",
9 | "start": "next start",
10 | "lint": "eslint src",
11 | "lint-fix": "pnpm lint --fix",
12 | "test-dev": "start-server-and-test dev 3000 test",
13 | "test-start": "start-server-and-test start 3000 test",
14 | "ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\""
15 | },
16 | "prettier": {
17 | "printWidth": 80,
18 | "trailingComma": "all",
19 | "singleQuote": true
20 | },
21 | "dependencies": {
22 | "@heroicons/react": "^1.0.6",
23 | "@hookform/resolvers": "^3.1.1",
24 | "@tailwindcss/typography": "^0.5.7",
25 | "@tanstack/react-query": "^4.2.3",
26 | "@trpc/client": "^10.34.0",
27 | "@trpc/next": "^10.34.0",
28 | "@trpc/react-query": "^10.34.0",
29 | "@trpc/server": "^10.34.0",
30 | "autoprefixer": "^10.4.11",
31 | "clsx": "^1.1.1",
32 | "next": "12.3.1",
33 | "next-auth": "^4.10.3",
34 | "prism-react-renderer": "^1.3.3",
35 | "react": "^18.2.0",
36 | "react-dom": "^18.2.0",
37 | "react-hook-form": "^7.45.2",
38 | "start-server-and-test": "^1.12.0",
39 | "superjson": "^1.9.1",
40 | "tailwindcss": "^3.1.8",
41 | "zod": "^3.16.0"
42 | },
43 | "devDependencies": {
44 | "@tailwindcss/forms": "^0.5.4",
45 | "@types/jest": "^29.2.4",
46 | "@types/node": "^17.0.33",
47 | "@types/react": "^18.0.9",
48 | "@typescript-eslint/eslint-plugin": "^4.30.0",
49 | "@typescript-eslint/parser": "^4.26.0",
50 | "cross-env": "^7.0.3",
51 | "eslint": "^7.32.0",
52 | "eslint-config-next": "^12.3.1",
53 | "eslint-config-prettier": "^8.5.0",
54 | "eslint-plugin-prettier": "^4.0.0",
55 | "eslint-plugin-react": "^7.29.4",
56 | "eslint-plugin-react-hooks": "^4.5.0",
57 | "eslint-plugin-simple-import-sort": "^7.0.0",
58 | "jest": "^29.3.1",
59 | "npm-run-all": "^4.1.5",
60 | "postcss": "^8.4.16",
61 | "prettier": "^2.6.2",
62 | "ts-jest": "^29.0.3",
63 | "ts-node": "^10.7.0",
64 | "typescript": "4.7.4"
65 | },
66 | "publishConfig": {
67 | "access": "restricted"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { httpBatchLink, loggerLink } from '@trpc/client';
2 | import { createTRPCNext } from '@trpc/next';
3 | // ℹ️ Type-only import:
4 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
5 | import type { AppRouter } from 'server/trpc/routers/index';
6 | import superjson from 'superjson';
7 |
8 | function getBaseUrl() {
9 | // browser should use relative url
10 | if (typeof window !== 'undefined') return '';
11 |
12 | // reference for vercel.com SSR
13 | if (process.env.VERCEL_URL) {
14 | return `https://${process.env.VERCEL_URL}`;
15 | }
16 |
17 | // reference for render.com SSR
18 | if (process.env.RENDER_INTERNAL_HOSTNAME) {
19 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
20 | }
21 |
22 | // override for docker etc SSR
23 | if (process.env.NEXT_PUBLIC_APP_URL) return process.env.NEXT_PUBLIC_APP_URL;
24 |
25 | // assume localhost in dev SSR
26 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
27 | }
28 |
29 | export const trpc = createTRPCNext({
30 | /**
31 | * Config options:
32 | * https://trpc.io/docs/v10/nextjs#createtrpcnext-options
33 | */
34 | config() {
35 | return {
36 | /**
37 | * @link https://trpc.io/docs/v10/data-transformers
38 | */
39 | transformer: superjson,
40 | /**
41 | * @link https://trpc.io/docs/v10/links
42 | */
43 | links: [
44 | // adds pretty logs to your console in development and logs errors in production
45 | loggerLink({
46 | enabled: () => true,
47 | }),
48 | httpBatchLink({
49 | url: `${getBaseUrl()}/api/trpc`,
50 | }),
51 | ],
52 | };
53 | },
54 | /**
55 | * @link https://trpc.io/docs/v10/ssr
56 | */
57 | ssr: false,
58 | });
59 |
60 | /**
61 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
62 | * @link https://trpc.io/docs/v10/react#2-create-trpc-hooks
63 | */
64 |
65 | /**
66 | * You can use inference to get types for procedure input and output
67 | * this is equivalent to inferQueryOutput/inferQueryInput/inferMutationOutput/inferMutationInput in v9
68 | * @example type SourceInput = inferProcedureInput;
69 | * @example type SourceOutput = inferProcedureOutput;
70 | */
71 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main, 0.x ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '27 0 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'typescript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/src/feature/react-hook-form/Form.tsx:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { useId } from 'react';
3 | import {
4 | FieldValues,
5 | FormProvider,
6 | SubmitHandler,
7 | useForm,
8 | useFormContext,
9 | UseFormProps,
10 | UseFormReturn,
11 | } from 'react-hook-form';
12 | import { z } from 'zod';
13 |
14 | type UseZodForm = UseFormReturn & {
15 | /**
16 | * A unique ID for this form.
17 | */
18 | id: string;
19 | };
20 | export function useZodForm(
21 | props: Omit, 'resolver'> & {
22 | schema: TSchema;
23 | },
24 | ) {
25 | const form = useForm({
26 | ...props,
27 | resolver: zodResolver(props.schema, undefined, {
28 | // This makes it so we can use `.transform()`s on the schema without same transform getting applied again when it reaches the server
29 | raw: true,
30 | }),
31 | }) as UseZodForm;
32 |
33 | form.id = useId();
34 |
35 | return form;
36 | }
37 |
38 | type AnyZodForm = UseZodForm;
39 |
40 | export function Form(
41 | props: Omit, 'onSubmit' | 'id'> & {
42 | handleSubmit: SubmitHandler;
43 | form: UseZodForm;
44 | },
45 | ) {
46 | const { handleSubmit, form, ...passThrough }: typeof props = props;
47 | return (
48 |
49 |
66 | );
67 | }
68 |
69 | export function SubmitButton(
70 | props: Omit, 'type' | 'form'> & {
71 | /**
72 | * Optionally specify a form to submit instead of the closest form context.
73 | */
74 | form?: AnyZodForm;
75 | },
76 | ) {
77 | const context = useFormContext();
78 |
79 | const form = props.form ?? context;
80 | if (!form) {
81 | throw new Error(
82 | 'SubmitButton must be used within a Form or have a form prop',
83 | );
84 | }
85 | const { formState } = form;
86 |
87 | return (
88 |
94 | {formState.isSubmitting ? 'Loading' : props.children}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/feature/react-hook-form/index.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from 'utils/trpc';
2 | import { z } from 'zod';
3 |
4 | import { Form, SubmitButton, useZodForm } from './Form';
5 |
6 | // validation schema is used by tRPC mutation and client
7 | export const validationSchema = z.object({
8 | title: z.string().min(2),
9 | text: z.string().min(5),
10 | });
11 |
12 | function AddPostForm() {
13 | const utils = trpc.useContext().reactHookFormRouter;
14 |
15 | const mutation = trpc.reactHookFormRouter.add.useMutation({
16 | onSuccess: async () => {
17 | await utils.list.invalidate();
18 | },
19 | });
20 |
21 | const form = useZodForm({
22 | schema: validationSchema,
23 | defaultValues: {
24 | title: '',
25 | text: '',
26 | },
27 | });
28 |
29 | return (
30 | <>
31 |
65 |
69 | Add post
70 |
71 | >
72 | );
73 | }
74 | export default function Page() {
75 | const [posts] = trpc.reactHookFormRouter.list.useSuspenseQuery();
76 |
77 | return (
78 | <>
79 | Posts
80 |
81 |
82 | {posts &&
83 | posts.map((post) => (
84 |
88 | {post.title}
89 | {post.text}
90 |
91 | ))}
92 |
93 |
94 | Add a post
95 |
96 |
97 | >
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/utils/ExamplePage.tsx:
--------------------------------------------------------------------------------
1 | import { EyeIcon } from '@heroicons/react/outline';
2 | import { CodeIcon } from '@heroicons/react/outline';
3 | import { CheckIcon, ClipboardCopyIcon, HomeIcon } from '@heroicons/react/solid';
4 | import Head from 'next/head';
5 | import Link from 'next/link';
6 | import { useRouter } from 'next/router';
7 | import Highlight, { defaultProps } from 'prism-react-renderer';
8 | import theme from 'prism-react-renderer/themes/vsDark';
9 | import { Fragment, ReactNode, Suspense, useEffect, useState } from 'react';
10 |
11 | import { ErrorBoundary } from './ClientSuspense';
12 | import { trpc } from './trpc';
13 | import { useClipboard } from './useClipboard';
14 |
15 | interface SourceFile {
16 | title: string;
17 | path: string;
18 | }
19 |
20 | function clsx(...classes: unknown[]) {
21 | return classes.filter(Boolean).join(' ');
22 | }
23 |
24 | export interface ExampleProps {
25 | title: string;
26 | href: string;
27 | /**
28 | * Only render this on the client
29 | */
30 | clientOnly?: boolean;
31 | /**
32 | * Summary - shown on home page
33 | */
34 | summary?: JSX.Element;
35 | /**
36 | * Detail page components
37 | */
38 | detail?: JSX.Element;
39 | /**
40 | * Files for "View Source" in the UI
41 | */
42 | files: SourceFile[];
43 | }
44 |
45 | export default function Breadcrumbs(props: {
46 | pages: { title: string; href: string }[];
47 | }) {
48 | const router = useRouter();
49 |
50 | return (
51 |
52 |
53 |
54 |
65 |
66 | {props.pages.map((page) => (
67 |
68 |
90 |
91 | ))}
92 |
93 |
94 | );
95 | }
96 |
97 | function Code(props: { contents: string; language: string }) {
98 | return (
99 |
105 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
106 |
110 | {tokens.map((line, i) => (
111 |
112 | {line.map((token, key) => (
113 |
114 | ))}
115 |
116 | ))}
117 |
118 | )}
119 |
120 | );
121 | }
122 |
123 | function basename(path: string) {
124 | return path.split('/').pop() ?? '';
125 | }
126 |
127 | function ViewSource(props: SourceFile) {
128 | const query = trpc.source.getSource.useQuery(
129 | { path: props.path },
130 | {
131 | cacheTime: Infinity,
132 | refetchOnWindowFocus: false,
133 | refetchOnMount: false,
134 | refetchOnReconnect: false,
135 | },
136 | );
137 |
138 | const filename = basename(props.path);
139 | const language = filename.split('.').pop() ?? '';
140 |
141 | const [hasCopied, copy] = useClipboard(query.data?.contents || '');
142 |
143 | if (!query.data) {
144 | return ;
145 | }
146 |
147 | return (
148 |
149 |
154 | {hasCopied ? (
155 | <>
156 | Copied
157 |
158 | >
159 | ) : (
160 | <>
161 | Copy
162 |
163 | >
164 | )}
165 |
166 |
167 |
168 | );
169 | }
170 |
171 | function Spinner() {
172 | return (
173 |
174 |
175 | ⏳
176 |
177 |
178 | );
179 | }
180 |
181 | function ClientOnly(props: { children: ReactNode }) {
182 | const [isMounted, setIsMounted] = useState(false);
183 | useEffect(() => {
184 | setIsMounted(true);
185 | }, []);
186 | if (!isMounted) {
187 | return ;
188 | }
189 | return <>{props.children}>;
190 | }
191 | export function ExamplePage(
192 | props: ExampleProps & {
193 | children?: ReactNode;
194 | },
195 | ) {
196 | const routerQuery = useRouter().query;
197 | const utils = trpc.useContext();
198 |
199 | useEffect(() => {
200 | for (const file of props.files) {
201 | utils.source.getSource.prefetch({ path: file.path });
202 | }
203 | }, [props.files, utils]);
204 |
205 | const innerContent = (
206 | }>
207 | {!routerQuery.file && props.children}
208 |
209 | {props.files.map((file) => (
210 |
211 | {file.path === routerQuery.file && }
212 |
213 | ))}
214 |
215 | );
216 | const content = props.clientOnly ? (
217 | {innerContent}
218 | ) : (
219 | innerContent
220 | );
221 | return (
222 | <>
223 |
224 | {props.title}
225 |
226 |
227 |
228 |
229 | {props.title}
230 |
231 |
232 |
{props.summary}
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
{props.title}
242 |
{props.detail || props.summary}
243 |
244 |
245 |
246 |
284 |
285 |
286 | {content}
287 |
288 |
289 |
290 |
291 | >
292 | );
293 | }
294 |
--------------------------------------------------------------------------------