├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma └── schema.prisma ├── public └── favicon.ico ├── src ├── env │ ├── client.mjs │ ├── schema.mjs │ └── server.mjs ├── pages │ ├── _app.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── do-tweet-processing.ts │ │ └── trpc │ │ │ └── [trpc].ts │ └── index.tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ └── example.ts │ │ └── trpc.ts │ ├── auth.ts │ ├── db.ts │ └── twitter.ts ├── styles │ └── globals.css ├── types │ └── next-auth.d.ts └── utils │ └── api.ts ├── tailwind.config.cjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. 2 | # Keep this file up-to-date when you add new variables to `.env`. 3 | 4 | # This file will be committed to version control, so make sure not to have any secrets in it. 5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. 6 | 7 | # When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly 8 | # Prisma 9 | DATABASE_URL=file:./db.sqlite 10 | 11 | # Next Auth 12 | # You can generate the secret via 'openssl rand -base64 32' on Linux 13 | # More info: https://next-auth.js.org/configuration/options#secret 14 | # NEXTAUTH 15 | NEXTAUTH_URL=http://localhost:3000 16 | 17 | # Next Auth Discord Provider 18 | DISCORD_CLIENT_ID= 19 | DISCORD_CLIENT_SECRET= 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "@typescript-eslint/consistent-type-imports": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.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 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # 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 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Prisma](https://prisma.io) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 4 | * This is especially useful for Docker builds. 5 | */ 6 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs")); 7 | 8 | /** @type {import("next").NextConfig} */ 9 | const config = { 10 | reactStrictMode: true, 11 | swcMinify: true, 12 | i18n: { 13 | locales: ["en"], 14 | defaultLocale: "en", 15 | }, 16 | }; 17 | export default config; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trash-mountain", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "postinstall": "prisma generate", 9 | "lint": "next lint", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "@next-auth/prisma-adapter": "1.0.5", 14 | "@prisma/client": "^4.5.0", 15 | "@tanstack/react-query": "^4.16.0", 16 | "@trpc/client": "^10.0.0", 17 | "@trpc/next": "^10.0.0", 18 | "@trpc/react-query": "^10.0.0", 19 | "@trpc/server": "^10.0.0", 20 | "@upstash/qstash": "^0.3.1", 21 | "next": "13.0.2", 22 | "next-auth": "4.17.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "superjson": "1.9.1", 26 | "twitter-api-sdk": "^1.2.1", 27 | "zod": "^3.18.0" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^18.0.0", 31 | "@types/react": "^18.0.14", 32 | "@types/react-dom": "^18.0.5", 33 | "@typescript-eslint/eslint-plugin": "^5.33.0", 34 | "@typescript-eslint/parser": "^5.33.0", 35 | "autoprefixer": "^10.4.7", 36 | "eslint": "^8.26.0", 37 | "eslint-config-next": "13.0.2", 38 | "postcss": "^8.4.14", 39 | "prettier": "^2.7.1", 40 | "prettier-plugin-tailwindcss": "^0.1.13", 41 | "prisma": "^4.5.0", 42 | "tailwindcss": "^3.2.0", 43 | "typescript": "^4.8.4" 44 | }, 45 | "ct3aMetadata": { 46 | "initVersion": "6.0.0-beta.3f2ae47" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | // NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below 11 | // Further reading: 12 | // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema 13 | // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | model Example { 18 | id String @id @default(cuid()) 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | } 22 | 23 | // Necessary for Next auth 24 | model Account { 25 | id String @id @default(cuid()) 26 | userId String 27 | type String 28 | provider String 29 | providerAccountId String 30 | refresh_token String? // @db.Text 31 | access_token String? // @db.Text 32 | expires_at Int? 33 | token_type String? 34 | scope String? 35 | id_token String? // @db.Text 36 | session_state String? 37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | 39 | @@unique([provider, providerAccountId]) 40 | } 41 | 42 | model Session { 43 | id String @id @default(cuid()) 44 | sessionToken String @unique 45 | userId String 46 | expires DateTime 47 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 48 | } 49 | 50 | model User { 51 | id String @id @default(cuid()) 52 | name String? 53 | email String? @unique 54 | emailVerified DateTime? 55 | image String? 56 | accounts Account[] 57 | sessions Session[] 58 | } 59 | 60 | model VerificationToken { 61 | identifier String 62 | token String @unique 63 | expires DateTime 64 | 65 | @@unique([identifier, token]) 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/trash-mountain/8f79087648a16cb7c7e01763ae28d64693467ef3/public/favicon.ico -------------------------------------------------------------------------------- /src/env/client.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { clientEnv, clientSchema } from "./schema.mjs"; 3 | 4 | const _clientEnv = clientSchema.safeParse(clientEnv); 5 | 6 | export const formatErrors = ( 7 | /** @type {import('zod').ZodFormattedError,string>} */ 8 | errors, 9 | ) => 10 | Object.entries(errors) 11 | .map(([name, value]) => { 12 | if (value && "_errors" in value) 13 | return `${name}: ${value._errors.join(", ")}\n`; 14 | }) 15 | .filter(Boolean); 16 | 17 | if (!_clientEnv.success) { 18 | console.error( 19 | "❌ Invalid environment variables:\n", 20 | ...formatErrors(_clientEnv.error.format()), 21 | ); 22 | throw new Error("Invalid environment variables"); 23 | } 24 | 25 | for (let key of Object.keys(_clientEnv.data)) { 26 | if (!key.startsWith("NEXT_PUBLIC_")) { 27 | console.warn( 28 | `❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`, 29 | ); 30 | 31 | throw new Error("Invalid public environment variable name"); 32 | } 33 | } 34 | 35 | export const env = _clientEnv.data; 36 | -------------------------------------------------------------------------------- /src/env/schema.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { z } from "zod"; 3 | 4 | /** 5 | * Specify your server-side environment variables schema here. 6 | * This way you can ensure the app isn't built with invalid env vars. 7 | */ 8 | export const serverSchema = z.object({ 9 | DATABASE_URL: z.string().url().optional(), 10 | NODE_ENV: z.enum(["development", "test", "production"]), 11 | NEXTAUTH_SECRET: 12 | process.env.NODE_ENV === "production" 13 | ? z.string().min(1) 14 | : z.string().min(1).optional(), 15 | NEXTAUTH_URL: z.preprocess( 16 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL 17 | // Since NextAuth.js automatically uses the VERCEL_URL if present. 18 | (str) => process.env.VERCEL_URL ?? str, 19 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL 20 | process.env.VERCEL ? z.string() : z.string().url() 21 | ), 22 | DISCORD_CLIENT_ID: z.string().optional(), 23 | DISCORD_CLIENT_SECRET: z.string().optional(), 24 | DISCORD_WEBHOOK_URL: z.string(), 25 | 26 | TWITTER_API_KEY: z.string(), 27 | TWITTER_API_SECRET: z.string(), 28 | TWITTER_API_BEARER_TOKEN: z.string(), 29 | 30 | CRON_TOKEN: z.string(), 31 | }); 32 | 33 | /** 34 | * Specify your client-side environment variables schema here. 35 | * This way you can ensure the app isn't built with invalid env vars. 36 | * To expose them to the client, prefix them with `NEXT_PUBLIC_`. 37 | */ 38 | export const clientSchema = z.object({ 39 | // NEXT_PUBLIC_CLIENTVAR: z.string(), 40 | }); 41 | 42 | /** 43 | * You can't destruct `process.env` as a regular object, so you have to do 44 | * it manually here. This is because Next.js evaluates this at build time, 45 | * and only used environment variables are included in the build. 46 | * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} 47 | */ 48 | export const clientEnv = { 49 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 50 | }; 51 | -------------------------------------------------------------------------------- /src/env/server.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.mjs`-file to be imported there. 5 | */ 6 | import { serverSchema } from "./schema.mjs"; 7 | import { env as clientEnv, formatErrors } from "./client.mjs"; 8 | 9 | const _serverEnv = serverSchema.safeParse(process.env); 10 | 11 | if (!_serverEnv.success) { 12 | console.error( 13 | "❌ Invalid environment variables:\n", 14 | ...formatErrors(_serverEnv.error.format()), 15 | ); 16 | throw new Error("Invalid environment variables"); 17 | } 18 | 19 | for (let key of Object.keys(_serverEnv.data)) { 20 | if (key.startsWith("NEXT_PUBLIC_")) { 21 | console.warn("❌ You are exposing a server-side env-variable:", key); 22 | 23 | throw new Error("You are exposing a server-side env-variable"); 24 | } 25 | } 26 | 27 | export const env = { ..._serverEnv.data, ...clientEnv }; 28 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { type AppType } from "next/app"; 2 | import { type Session } from "next-auth"; 3 | import { SessionProvider } from "next-auth/react"; 4 | 5 | import { api } from "../utils/api"; 6 | 7 | import "../styles/globals.css"; 8 | 9 | const MyApp: AppType<{ session: Session | null }> = ({ 10 | Component, 11 | pageProps: { session, ...pageProps }, 12 | }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default api.withTRPC(MyApp); 21 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type NextAuthOptions } from "next-auth"; 2 | import DiscordProvider from "next-auth/providers/discord"; 3 | // Prisma adapter for NextAuth, optional and can be removed 4 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 5 | 6 | import { env } from "../../../env/server.mjs"; 7 | import { prisma } from "../../../server/db"; 8 | 9 | export const authOptions: NextAuthOptions = { 10 | // Include user.id on session 11 | callbacks: { 12 | session({ session, user }) { 13 | if (session.user) { 14 | session.user.id = user.id; 15 | } 16 | return session; 17 | }, 18 | }, 19 | // Configure one or more authentication providers 20 | adapter: PrismaAdapter(prisma), 21 | providers: [ 22 | DiscordProvider({ 23 | clientId: env.DISCORD_CLIENT_ID!, 24 | clientSecret: env.DISCORD_CLIENT_SECRET!, 25 | }), 26 | // ...add more providers here 27 | ], 28 | }; 29 | 30 | export default NextAuth(authOptions); 31 | -------------------------------------------------------------------------------- /src/pages/api/do-tweet-processing.ts: -------------------------------------------------------------------------------- 1 | import type { inferAsyncReturnType } from "@trpc/server"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { env } from "../../env/server.mjs"; 4 | import { getRecentTrashTweets } from "../../server/twitter"; 5 | import { verifySignature } from "@upstash/qstash/nextjs"; 6 | 7 | const sendTweetToDiscord = async ( 8 | tweet: NonNullable< 9 | inferAsyncReturnType["data"] 10 | >[number] 11 | ) => { 12 | const formattedMessage = `Steal this tweet: https://twitter.com/trashh_dev/status/${tweet.id}`; 13 | 14 | const webhookUrl = env.DISCORD_WEBHOOK_URL; 15 | 16 | return fetch(webhookUrl, { 17 | method: "post", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ 22 | content: formattedMessage, 23 | }), 24 | }); 25 | }; 26 | 27 | const doTweetProcessing = async (req: NextApiRequest, res: NextApiResponse) => { 28 | // const tokenFromRequest = req.headers.authorization; 29 | 30 | // if (tokenFromRequest !== env.CRON_TOKEN) { 31 | // res.status(401).json({ error: "Unauthorized" }); 32 | // return; 33 | // } 34 | 35 | const recentTweets = await getRecentTrashTweets(); 36 | 37 | const goodTrashTweets = 38 | recentTweets?.data?.filter( 39 | (tweet) => 40 | (tweet?.public_metrics?.like_count ?? 0) > 500 && 41 | new Date(tweet?.created_at ?? 0) > new Date(Date.now() - 86400000) 42 | ) ?? []; 43 | 44 | const discordWebhookCalls = goodTrashTweets?.map((tweet) => { 45 | return sendTweetToDiscord(tweet); 46 | }); 47 | 48 | await Promise.allSettled(discordWebhookCalls); 49 | 50 | return res.status(200).json(goodTrashTweets); 51 | }; 52 | 53 | export default verifySignature(doTweetProcessing); 54 | 55 | export const config = { 56 | api: { 57 | bodyParser: false, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "../../../env/server.mjs"; 4 | import { createInternalTrpcContext } from "../../../server/api/trpc"; 5 | import { appRouter } from "../../../server/api/root"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext: createInternalTrpcContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error(`❌ tRPC failed on ${path}: ${error}`); 15 | } 16 | : undefined, 17 | }); 18 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { type NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { signIn, signOut, useSession } from "next-auth/react"; 5 | 6 | import { api } from "../utils/api"; 7 | 8 | const Home: NextPage = () => { 9 | const hello = api.example.hello.useQuery({ text: "from tRPC" }); 10 | 11 | return ( 12 | <> 13 | 14 | Create T3 App 15 | 16 | 17 | 18 |
19 |
20 |

21 | Create T3 App 22 |

23 |
24 | 29 |

First Steps →

30 |
31 | Just the basics - Everything you need to know to set up your 32 | database and authentication. 33 |
34 | 35 | 40 |

Documentation →

41 |
42 | Learn more about Create T3 App, the libraries it uses, and how 43 | to deploy it. 44 |
45 | 46 |
47 |
48 |

49 | {hello.data ? hello.data.greeting : "Loading tRPC query..."} 50 |

51 | 52 |
53 |
54 |
55 | 56 | ); 57 | }; 58 | 59 | export default Home; 60 | 61 | const AuthShowcase: React.FC = () => { 62 | const { data: sessionData } = useSession(); 63 | 64 | const { data: secretMessage } = api.example.getSecretMessage.useQuery( 65 | undefined, // no input 66 | { enabled: sessionData?.user !== undefined }, 67 | ); 68 | 69 | return ( 70 |
71 |

72 | {sessionData && Logged in as {sessionData.user?.name}} 73 | {secretMessage && - {secretMessage}} 74 |

75 | 81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { createTrpcRouter } from "./trpc"; 2 | import { exampleRouter } from "./routers/example"; 3 | 4 | /** 5 | * This is the primary router for your server. 6 | * 7 | * All routers added in /api/routers should be manually added here 8 | */ 9 | export const appRouter = createTrpcRouter({ 10 | example: exampleRouter, 11 | }); 12 | 13 | // export type definition of API 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /src/server/api/routers/example.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { createTrpcRouter, publicProcedure, protectedProcedure } from "../trpc"; 4 | 5 | export const exampleRouter = createTrpcRouter({ 6 | hello: publicProcedure 7 | .input(z.object({ text: z.string() })) 8 | .query(({ input }) => { 9 | return { 10 | greeting: `Hello ${input.text}`, 11 | }; 12 | }), 13 | 14 | getAll: publicProcedure.query(({ ctx }) => { 15 | return ctx.prisma.example.findMany(); 16 | }), 17 | 18 | getSecretMessage: protectedProcedure.query(() => { 19 | return "you can now see this secret message!"; 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /src/server/api/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE 3 | * 4 | * tl;dr - this is where all the tRPC server stuff is created and plugged in. 5 | * The pieces you will need to use are documented accordingly near the end 6 | */ 7 | 8 | /** 9 | * 1. CONTEXT 10 | * 11 | * This section defines the "contexts" that are available in the backend API 12 | * 13 | * These allow you to access things like the database, the session, etc, when 14 | * processing a request 15 | * 16 | */ 17 | import { type inferAsyncReturnType } from "@trpc/server"; 18 | import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; 19 | import { type Session } from "next-auth"; 20 | 21 | import { getServerAuthSession } from "../auth"; 22 | import { prisma } from "../db"; 23 | 24 | type CreateContextOptions = { 25 | session: Session | null; 26 | }; 27 | 28 | /** 29 | * This helper generates the "internals" for a tRPC context. If you need to use 30 | * it, you can export it from here 31 | * 32 | * Examples of things you may need it for: 33 | * - testing, so we dont have to mock Next.js' req/res 34 | * - trpc's `createSSGHelpers` where we don't have req/res 35 | * @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts 36 | */ 37 | const createContextInner = async (opts: CreateContextOptions) => { 38 | return { 39 | session: opts.session, 40 | prisma, 41 | }; 42 | }; 43 | 44 | /** 45 | * This is the actual context you'll use in your router. It will be used to 46 | * process every request that goes through your tRPC endpoint 47 | * @link https://trpc.io/docs/context 48 | */ 49 | export const createInternalTrpcContext = async ( 50 | opts: CreateNextContextOptions, 51 | ) => { 52 | const { req, res } = opts; 53 | 54 | // Get the session from the server using the unstable_getServerSession wrapper function 55 | const session = await getServerAuthSession({ req, res }); 56 | 57 | return await createContextInner({ 58 | session, 59 | }); 60 | }; 61 | 62 | /** 63 | * 2. INITIALIZATION 64 | * 65 | * This is where the trpc api is initialized, connecting the context and 66 | * transformer 67 | */ 68 | import { initTRPC, TRPCError } from "@trpc/server"; 69 | import superjson from "superjson"; 70 | 71 | const t = initTRPC 72 | .context>() 73 | .create({ 74 | transformer: superjson, 75 | errorFormatter({ shape }) { 76 | return shape; 77 | }, 78 | }); 79 | 80 | /** 81 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) 82 | * 83 | * These are the pieces you use to build your tRPC API. You should import these 84 | * a lot in the /src/server/api/routers folder 85 | */ 86 | 87 | /** 88 | * This is how you create new routers and subrouters in your tRPC API 89 | * @see https://trpc.io/docs/router 90 | */ 91 | export const createTrpcRouter = t.router; 92 | 93 | /** 94 | * Public (unauthed) procedure 95 | * 96 | * This is the base piece you use to build new queries and mutations on your 97 | * tRPC API. It does not guarantee that a user querying is authorized, but you 98 | * can still access user session data if they are logged in 99 | */ 100 | export const publicProcedure = t.procedure; 101 | 102 | /** 103 | * Reusable middleware that enforces users are logged in before running the 104 | * procedure 105 | */ 106 | const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { 107 | if (!ctx.session || !ctx.session.user) { 108 | throw new TRPCError({ code: "UNAUTHORIZED" }); 109 | } 110 | return next({ 111 | ctx: { 112 | // infers the `session` as non-nullable 113 | session: { ...ctx.session, user: ctx.session.user }, 114 | }, 115 | }); 116 | }); 117 | 118 | /** 119 | * Public (protected) procedure 120 | * 121 | * This is the base piece you use to build new queries and mutations on your 122 | * tRPC API. It does not guarantee that a user querying is authorized, but you 123 | * can still access user session data if they are logged in 124 | * 125 | * @see https://trpc.io/docs/procedures 126 | */ 127 | export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); 128 | -------------------------------------------------------------------------------- /src/server/auth.ts: -------------------------------------------------------------------------------- 1 | import { type GetServerSidePropsContext } from "next"; 2 | import { unstable_getServerSession } from "next-auth"; 3 | 4 | import { authOptions } from "../pages/api/auth/[...nextauth]"; 5 | 6 | /** 7 | * Wrapper for unstable_getServerSession, used in trpc createContext and the 8 | * restricted API route 9 | * 10 | * Don't worry too much about the "unstable", it's safe to use but the syntax 11 | * may change in future versions 12 | * 13 | * @see https://next-auth.js.org/configuration/nextjs 14 | */ 15 | 16 | export const getServerAuthSession = async (ctx: { 17 | req: GetServerSidePropsContext["req"]; 18 | res: GetServerSidePropsContext["res"]; 19 | }) => { 20 | return await unstable_getServerSession(ctx.req, ctx.res, authOptions); 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "../env/server.mjs"; 4 | 5 | declare global { 6 | // eslint-disable-next-line no-var 7 | var prisma: PrismaClient | undefined; 8 | } 9 | 10 | export const prisma = 11 | global.prisma || 12 | new PrismaClient({ 13 | log: 14 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 15 | }); 16 | 17 | if (env.NODE_ENV !== "production") { 18 | global.prisma = prisma; 19 | } 20 | -------------------------------------------------------------------------------- /src/server/twitter.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "twitter-api-sdk"; 2 | import { env } from "../env/server.mjs"; 3 | 4 | export async function getRecentTrashTweets() { 5 | const client = new Client(env.TWITTER_API_BEARER_TOKEN); 6 | 7 | const response = await client.tweets.tweetsRecentSearch({ 8 | query: "from:trashh_dev has:images -is:reply -is:retweet", 9 | "tweet.fields": ["public_metrics", "created_at"], 10 | }); 11 | 12 | return response; 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { type DefaultSession } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user?: { 9 | id: string; 10 | } & DefaultSession["user"]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink, loggerLink } from "@trpc/client"; 2 | import { createTRPCNext } from "@trpc/next"; 3 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; 4 | import superjson from "superjson"; 5 | 6 | import { type AppRouter } from "../server/api/root"; 7 | 8 | const getBaseUrl = () => { 9 | if (typeof window !== "undefined") return ""; // browser should use relative url 10 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 11 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 12 | }; 13 | 14 | export const api = createTRPCNext({ 15 | config() { 16 | return { 17 | transformer: superjson, 18 | links: [ 19 | loggerLink({ 20 | enabled: (opts) => 21 | process.env.NODE_ENV === "development" || 22 | (opts.direction === "down" && opts.result instanceof Error), 23 | }), 24 | httpBatchLink({ 25 | url: `${getBaseUrl()}/api/trpc`, 26 | }), 27 | ], 28 | }; 29 | }, 30 | ssr: false, 31 | }); 32 | 33 | /** 34 | * Inference helper for inputs 35 | * @example type HelloInput = RouterInputs['example']['hello'] 36 | **/ 37 | export type RouterInputs = inferRouterInputs; 38 | /** 39 | * Inference helper for outputs 40 | * @example type HelloOutput = RouterOutputs['example']['hello'] 41 | **/ 42 | export type RouterOutputs = inferRouterOutputs; 43 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "noUncheckedIndexedAccess": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------