├── src ├── styles │ └── globals.css ├── pages │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── _document.tsx │ ├── _app.tsx │ ├── index.tsx │ └── posts.tsx ├── utils │ ├── cn.ts │ ├── zod-form.ts │ └── api.ts ├── server │ ├── db.ts │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ └── post.ts │ │ └── trpc.ts │ └── auth.ts ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── text-area.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── select.tsx │ └── dialog.tsx └── env.mjs ├── public └── favicon.ico ├── .prettierignore ├── postcss.config.cjs ├── renovate.json ├── .vscode ├── extensions.json └── settings.json ├── docker-compose.yml ├── vitest.config.ts ├── e2e ├── setup │ ├── storage-state.json │ └── global.ts └── smoke.test.ts ├── playwright.config.ts ├── .gitignore ├── tsconfig.json ├── next.config.mjs ├── .eslintrc.cjs ├── tailwind.config.ts ├── .env.example ├── prettier.config.cjs ├── test └── trpc │ └── post-router.test.ts ├── prisma └── schema.prisma ├── .github └── workflows │ └── ci.yml ├── package.json └── README.md /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliusmarminge/t3-complete/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules/** 3 | .next 4 | .vercel 5 | e2e/setup/storage-state.json -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "automerge": true 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import { authOptions } from "~/server/auth"; 4 | 5 | export default NextAuth(authOptions); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "bradlc.vscode-tailwindcss", 6 | "Prisma.prisma" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | db: 5 | image: postgres:16 6 | restart: always 7 | environment: 8 | POSTGRES_PASSWORD: ${PGPASSWORD} 9 | volumes: 10 | - ./prisma/data:/var/lib/postgresql/data 11 | ports: 12 | - 5432:5432 13 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { configDefaults, defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | exclude: [...configDefaults.exclude, "**/e2e/**"], 7 | }, 8 | resolve: { 9 | alias: { 10 | "~/": join(__dirname, "./src/"), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/setup/storage-state.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookies": [ 3 | { 4 | "name": "next-auth.session-token", 5 | "value": "d52f0c50-b8e3-4326-b48c-4d4a66fdeb64", 6 | "domain": "localhost", 7 | "path": "/", 8 | "expires": -1, 9 | "httpOnly": true, 10 | "secure": false, 11 | "sameSite": "Lax" 12 | } 13 | ], 14 | "origins": [] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "tailwindCSS.experimental.classRegex": [ 8 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] 9 | ], 10 | "typescript.tsdk": "node_modules/typescript/lib" 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "~/env.mjs"; 4 | 5 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; 6 | 7 | export const prisma = 8 | globalForPrisma.prisma || 9 | new PrismaClient({ 10 | log: 11 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 12 | }); 13 | 14 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 15 | -------------------------------------------------------------------------------- /src/utils/zod-form.ts: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useForm, type UseFormProps } from "react-hook-form"; 3 | import { type ZodType } from "zod"; 4 | 5 | export function useZodForm( 6 | props: Omit, "resolver"> & { 7 | schema: TSchema; 8 | }, 9 | ) { 10 | const form = useForm({ 11 | ...props, 12 | resolver: zodResolver(props.schema, undefined), 13 | }); 14 | 15 | return form; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "~/env.mjs"; 4 | import { appRouter } from "~/server/api/root"; 5 | import { createTRPCContext } from "~/server/api/trpc"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext: createTRPCContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error( 15 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`, 16 | ); 17 | } 18 | : undefined, 19 | }); 20 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, type PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || "http://localhost:3000"; 4 | 5 | const opts = { 6 | headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS, 7 | // collectCoverage: !!process.env.PLAYWRIGHT_HEADLESS 8 | }; 9 | 10 | const config: PlaywrightTestConfig = { 11 | testDir: "./e2e", 12 | globalSetup: "./e2e/setup/global.ts", 13 | use: { 14 | ...devices["Desktop Chrome"], 15 | storageState: "./e2e/setup/storage-state.json", 16 | baseURL: baseUrl, 17 | headless: opts.headless, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /src/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "~/utils/cn"; 7 | 8 | const Label = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Label.displayName = LabelPrimitive.Root.displayName; 22 | 23 | export { Label }; 24 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { postRouter } from "./routers/post"; 4 | import { createTRPCRouter, publicProcedure } from "./trpc"; 5 | 6 | /** 7 | * This is the primary router for your server. 8 | * 9 | * All routers added in /api/routers should be manually added here 10 | */ 11 | export const appRouter = createTRPCRouter({ 12 | hello: publicProcedure 13 | .input(z.object({ text: z.string().nullish() })) 14 | .query(({ input }) => { 15 | return { 16 | greeting: `Hello from tRPC, ${input.text ?? "Anonymous"}`, 17 | }; 18 | }), 19 | post: postRouter, 20 | }); 21 | 22 | // export type definition of API 23 | export type AppRouter = typeof appRouter; 24 | -------------------------------------------------------------------------------- /.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 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | /prisma/data 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": 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 | "incremental": true, 18 | "noUncheckedIndexedAccess": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": ["./src/*"] 22 | } 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "**/*.cjs", 29 | "**/*.mjs", 30 | ".eslintrc.cjs" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 5 | * This is especially useful for Docker builds. 6 | */ 7 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs")); 8 | 9 | /** @type {import("next").NextConfig} */ 10 | const config = { 11 | reactStrictMode: true, 12 | 13 | /** 14 | * If you have the "experimental: { appDir: true }" setting enabled, then you 15 | * must comment the below `i18n` config out. 16 | * 17 | * @see https://github.com/vercel/next.js/issues/41980 18 | */ 19 | i18n: { 20 | locales: ["en"], 21 | defaultLocale: "en", 22 | }, 23 | 24 | // We run these separately in CI, so we can skip them here. 25 | eslint: { ignoreDuringBuilds: true }, 26 | typescript: { ignoreBuildErrors: true }, 27 | }; 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { type AppType } from "next/app"; 2 | import { Inter as FontSans } from "next/font/google"; 3 | import { type Session } from "next-auth"; 4 | import { SessionProvider } from "next-auth/react"; 5 | 6 | import "~/styles/globals.css"; 7 | import { api } from "~/utils/api"; 8 | 9 | const fontSans = FontSans({ 10 | subsets: ["latin"], 11 | variable: "--font-sans", 12 | }); 13 | 14 | const MyApp: AppType<{ session: Session | null }> = ({ 15 | Component, 16 | pageProps: { session, ...pageProps }, 17 | }) => { 18 | return ( 19 | <> 20 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default api.withTRPC(MyApp); 35 | -------------------------------------------------------------------------------- /src/ui/input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "~/utils/cn"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 8 | export interface InputProps 9 | extends React.InputHTMLAttributes {} 10 | 11 | const Input = React.forwardRef( 12 | ({ className, ...props }, ref) => { 13 | return ( 14 | 22 | ); 23 | }, 24 | ); 25 | Input.displayName = "Input"; 26 | 27 | export { Input }; 28 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type{import("eslint").Linter.Config} */ 2 | module.exports = { 3 | overrides: [ 4 | { 5 | extends: [ 6 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 7 | ], 8 | files: ["*.ts", "*.tsx"], 9 | excludedFiles: ["**/*.test.ts", "**/*.test.tsx"], 10 | parserOptions: { 11 | project: "tsconfig.json", 12 | }, 13 | }, 14 | ], 15 | parser: "@typescript-eslint/parser", 16 | parserOptions: { 17 | project: "./tsconfig.json", 18 | }, 19 | plugins: ["@typescript-eslint"], 20 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 21 | rules: { 22 | "@typescript-eslint/consistent-type-imports": "warn", 23 | "@typescript-eslint/no-misused-promises": [ 24 | 2, 25 | { 26 | checksVoidReturn: { 27 | // Allow promises to be used as attributes such as `onSubmit` 28 | attributes: false, 29 | }, 30 | }, 31 | ], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/ui/text-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "~/utils/cn"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 8 | export interface TextareaProps 9 | extends React.TextareaHTMLAttributes {} 10 | 11 | const Textarea = React.forwardRef( 12 | ({ className, ...props }, ref) => { 13 | return ( 14 |