Initial commit
This commit is contained in:
7
apps/auth-proxy/.env.example
Normal file
7
apps/auth-proxy/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
AUTH_SECRET=""
|
||||
AUTH_DISCORD_ID=""
|
||||
AUTH_DISCORD_SECRET=""
|
||||
AUTH_REDIRECT_PROXY_URL=""
|
||||
|
||||
NITRO_PRESET="vercel_edge"
|
||||
16
apps/auth-proxy/README.md
Normal file
16
apps/auth-proxy/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Auth Proxy
|
||||
|
||||
This is a simple proxy server that enables OAuth authentication for preview environments.
|
||||
|
||||
## Setup
|
||||
|
||||
Deploy it somewhere (Vercel is a one-click, zero-config option) and set the following environment variables:
|
||||
|
||||
- `AUTH_DISCORD_ID` - The Discord OAuth client ID
|
||||
- `AUTH_DISCORD_SECRET` - The Discord OAuth client secret
|
||||
- `AUTH_REDIRECT_PROXY_URL` - The URL of this proxy server
|
||||
- `AUTH_SECRET` - Your secret
|
||||
|
||||
Make sure the `AUTH_SECRET` and `AUTH_REDIRECT_PROXY_URL` match the values set for the main application's deployment for preview environments, and that you're using the same OAuth credentials for the proxy and the application's preview environment. The lines below shows what values should match eachother in both deployments.
|
||||
|
||||

|
||||
33
apps/auth-proxy/package.json
Normal file
33
apps/auth-proxy/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@acme/auth-proxy",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nitro build",
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"dev": "nitro dev --port 3001",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.18.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@acme/eslint-config": "workspace:^0.2.0",
|
||||
"@acme/prettier-config": "workspace:^0.1.0",
|
||||
"@acme/tailwind-config": "workspace:^0.1.0",
|
||||
"@acme/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^8.53.0",
|
||||
"nitropack": "^2.8.1",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@acme/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@acme/prettier-config"
|
||||
}
|
||||
17
apps/auth-proxy/routes/[...auth].ts
Normal file
17
apps/auth-proxy/routes/[...auth].ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Auth } from "@auth/core";
|
||||
import Discord from "@auth/core/providers/discord";
|
||||
import { eventHandler, toWebRequest } from "h3";
|
||||
|
||||
export default eventHandler(async (event) =>
|
||||
Auth(toWebRequest(event), {
|
||||
secret: process.env.AUTH_SECRET,
|
||||
trustHost: !!process.env.VERCEL,
|
||||
redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL,
|
||||
providers: [
|
||||
Discord({
|
||||
clientId: process.env.AUTH_DISCORD_ID,
|
||||
clientSecret: process.env.AUTH_DISCORD_SECRET,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
4
apps/auth-proxy/tsconfig.json
Normal file
4
apps/auth-proxy/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@acme/tsconfig/base.json",
|
||||
"include": ["routes"]
|
||||
}
|
||||
28
apps/nextjs/README.md
Normal file
28
apps/nextjs/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Create T3 App
|
||||
|
||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
|
||||
|
||||
## What's next? How do I make an app with this?
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
- [Next.js](https://nextjs.org)
|
||||
- [NextAuth.js](https://next-auth.js.org)
|
||||
- [Drizzle](https://orm.drizzle.team)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [tRPC](https://trpc.io)
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
|
||||
|
||||
- [Documentation](https://create.t3.gg/)
|
||||
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
|
||||
|
||||
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
|
||||
|
||||
## How do I deploy this?
|
||||
|
||||
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.
|
||||
15
apps/nextjs/next.config.mjs
Normal file
15
apps/nextjs/next.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Importing env files here to validate on build
|
||||
import "./src/env.mjs";
|
||||
import "@acme/auth/env.mjs";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
/** Enables hot reloading for local packages without a build step */
|
||||
transpilePackages: ["@acme/api", "@acme/auth", "@acme/db"],
|
||||
/** We already do linting and typechecking as separate tasks in CI */
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
};
|
||||
|
||||
export default config;
|
||||
56
apps/nextjs/package.json
Normal file
56
apps/nextjs/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@acme/nextjs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm with-env next build",
|
||||
"clean": "git clean -xdf .next .turbo node_modules",
|
||||
"dev": "pnpm with-env next dev",
|
||||
"lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"start": "pnpm with-env next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@acme/api": "workspace:^0.1.0",
|
||||
"@acme/auth": "workspace:^0.1.0",
|
||||
"@acme/db": "workspace:^0.1.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "^5.8.7",
|
||||
"@tanstack/react-query-devtools": "^5.8.7",
|
||||
"@tanstack/react-query-next-experimental": "5.8.7",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"next": "^14.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"superjson": "2.2.1",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@acme/eslint-config": "workspace:^0.2.0",
|
||||
"@acme/prettier-config": "workspace:^0.1.0",
|
||||
"@acme/tailwind-config": "workspace:^0.1.0",
|
||||
"@acme/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^18.18.13",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.53.0",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "3.3.5",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@acme/eslint-config/base",
|
||||
"@acme/eslint-config/nextjs",
|
||||
"@acme/eslint-config/react"
|
||||
]
|
||||
},
|
||||
"prettier": "@acme/prettier-config"
|
||||
}
|
||||
6
apps/nextjs/postcss.config.cjs
Normal file
6
apps/nextjs/postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
apps/nextjs/public/favicon.ico
Normal file
BIN
apps/nextjs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
13
apps/nextjs/public/t3-icon.svg
Normal file
13
apps/nextjs/public/t3-icon.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="258" height="198" viewBox="0 0 258 198" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_12)">
|
||||
<path d="M165.269 24.0976L188.481 -0.000411987H0V24.0976H165.269Z" fill="black"/>
|
||||
<path d="M163.515 95.3516L253.556 2.71059H220.74L145.151 79.7886L163.515 95.3516Z" fill="black"/>
|
||||
<path d="M233.192 130.446C233.192 154.103 214.014 173.282 190.357 173.282C171.249 173.282 155.047 160.766 149.534 143.467L146.159 132.876L126.863 152.171L128.626 156.364C138.749 180.449 162.568 197.382 190.357 197.382C227.325 197.382 257.293 167.414 257.293 130.446C257.293 105.965 243.933 84.7676 224.49 73.1186L219.929 70.3856L202.261 88.2806L210.322 92.5356C223.937 99.7236 233.192 114.009 233.192 130.446Z" fill="black"/>
|
||||
<path d="M87.797 191.697V44.6736H63.699V191.697H87.797Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_12">
|
||||
<rect width="258" height="198" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 923 B |
39
apps/nextjs/src/app/_components/auth-showcase.tsx
Normal file
39
apps/nextjs/src/app/_components/auth-showcase.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { auth, signIn, signOut } from "@acme/auth";
|
||||
|
||||
export async function AuthShowcase() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signIn("discord");
|
||||
}}
|
||||
>
|
||||
<button className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20">
|
||||
Sign in with Discord
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-center text-2xl text-white">
|
||||
{session && <span>Logged in as {session.user.name}</span>}
|
||||
</p>
|
||||
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signOut();
|
||||
}}
|
||||
>
|
||||
<button className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
apps/nextjs/src/app/_components/posts.tsx
Normal file
148
apps/nextjs/src/app/_components/posts.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import type { RouterOutputs } from "~/utils/api";
|
||||
|
||||
export function CreatePostForm() {
|
||||
const context = api.useContext();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
|
||||
const { mutateAsync: createPost, error } = api.post.create.useMutation({
|
||||
async onSuccess() {
|
||||
setTitle("");
|
||||
setContent("");
|
||||
await context.post.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex w-full max-w-2xl flex-col"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await createPost({
|
||||
title,
|
||||
content,
|
||||
});
|
||||
setTitle("");
|
||||
setContent("");
|
||||
await context.post.all.invalidate();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="mb-2 rounded bg-white/10 p-2 text-white"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Title"
|
||||
/>
|
||||
{error?.data?.zodError?.fieldErrors.title && (
|
||||
<span className="mb-2 text-red-500">
|
||||
{error.data.zodError.fieldErrors.title}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
className="mb-2 rounded bg-white/10 p-2 text-white"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Content"
|
||||
/>
|
||||
{error?.data?.zodError?.fieldErrors.content && (
|
||||
<span className="mb-2 text-red-500">
|
||||
{error.data.zodError.fieldErrors.content}
|
||||
</span>
|
||||
)}
|
||||
{}
|
||||
<button type="submit" className="rounded bg-pink-400 p-2 font-bold">
|
||||
Create
|
||||
</button>
|
||||
{error?.data?.code === "UNAUTHORIZED" && (
|
||||
<span className="mt-2 text-red-500">You must be logged in to post</span>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function PostList() {
|
||||
const [posts] = api.post.all.useSuspenseQuery();
|
||||
|
||||
if (posts.length === 0) {
|
||||
return (
|
||||
<div className="relative flex w-full flex-col gap-4">
|
||||
<PostCardSkeleton pulse={false} />
|
||||
<PostCardSkeleton pulse={false} />
|
||||
<PostCardSkeleton pulse={false} />
|
||||
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/10">
|
||||
<p className="text-2xl font-bold text-white">No posts yet</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{posts.map((p) => {
|
||||
return <PostCard key={p.id} post={p} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PostCard(props: {
|
||||
post: RouterOutputs["post"]["all"][number];
|
||||
}) {
|
||||
const context = api.useContext();
|
||||
const deletePost = api.post.delete.useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row rounded-lg bg-white/10 p-4 transition-all hover:scale-[101%]">
|
||||
<div className="flex-grow">
|
||||
<h2 className="text-2xl font-bold text-pink-400">{props.post.title}</h2>
|
||||
<p className="mt-2 text-sm">{props.post.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="cursor-pointer text-sm font-bold uppercase text-pink-400"
|
||||
onClick={async () => {
|
||||
await deletePost.mutateAsync(props.post.id);
|
||||
await context.post.all.invalidate();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PostCardSkeleton(props: { pulse?: boolean }) {
|
||||
const { pulse = true } = props;
|
||||
return (
|
||||
<div className="flex flex-row rounded-lg bg-white/10 p-4 transition-all hover:scale-[101%]">
|
||||
<div className="flex-grow">
|
||||
<h2
|
||||
className={`w-1/4 rounded bg-pink-400 text-2xl font-bold ${
|
||||
pulse && "animate-pulse"
|
||||
}`}
|
||||
>
|
||||
|
||||
</h2>
|
||||
<p
|
||||
className={`mt-2 w-1/3 rounded bg-current text-sm ${
|
||||
pulse && "animate-pulse"
|
||||
}`}
|
||||
>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Normal file
3
apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { GET, POST } from "@acme/auth";
|
||||
|
||||
export const runtime = "edge";
|
||||
42
apps/nextjs/src/app/api/trpc/[trpc]/route.ts
Normal file
42
apps/nextjs/src/app/api/trpc/[trpc]/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
|
||||
import { appRouter, createTRPCContext } from "@acme/api";
|
||||
import { auth } from "@acme/auth";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
/**
|
||||
* Configure basic CORS headers
|
||||
* You should extend this to match your needs
|
||||
*/
|
||||
function setCorsHeaders(res: Response) {
|
||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
||||
res.headers.set("Access-Control-Request-Method", "*");
|
||||
res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
|
||||
res.headers.set("Access-Control-Allow-Headers", "*");
|
||||
}
|
||||
|
||||
export function OPTIONS() {
|
||||
const response = new Response(null, {
|
||||
status: 204,
|
||||
});
|
||||
setCorsHeaders(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
const handler = auth(async (req) => {
|
||||
const response = await fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
router: appRouter,
|
||||
req,
|
||||
createContext: () => createTRPCContext({ auth: req.auth, req }),
|
||||
onError({ error, path }) {
|
||||
console.error(`>>> tRPC Error on '${path}'`, error);
|
||||
},
|
||||
});
|
||||
|
||||
setCorsHeaders(response);
|
||||
return response;
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
48
apps/nextjs/src/app/layout.tsx
Normal file
48
apps/nextjs/src/app/layout.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import "~/styles/globals.css";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { TRPCReactProvider } from "./providers";
|
||||
|
||||
const fontSans = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
/**
|
||||
* Since we're passing `headers()` to the `TRPCReactProvider` we need to
|
||||
* make the entire app dynamic. You can move the `TRPCReactProvider` further
|
||||
* down the tree (e.g. /dashboard and onwards) to make part of the app statically rendered.
|
||||
*/
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create T3 Turbo",
|
||||
description: "Simple monorepo with shared backend for web & mobile apps",
|
||||
openGraph: {
|
||||
title: "Create T3 Turbo",
|
||||
description: "Simple monorepo with shared backend for web & mobile apps",
|
||||
url: "https://create-t3-turbo.vercel.app",
|
||||
siteName: "Create T3 Turbo",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@jullerino",
|
||||
creator: "@jullerino",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||
<TRPCReactProvider headers={headers()}>
|
||||
{props.children}
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
38
apps/nextjs/src/app/page.tsx
Normal file
38
apps/nextjs/src/app/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { AuthShowcase } from "./_components/auth-showcase";
|
||||
import {
|
||||
CreatePostForm,
|
||||
PostCardSkeleton,
|
||||
PostList,
|
||||
} from "./_components/posts";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="flex h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<div className="container mt-12 flex flex-col items-center justify-center gap-4 py-8">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||
Create <span className="text-pink-400">T3</span> Turbo
|
||||
</h1>
|
||||
<AuthShowcase />
|
||||
|
||||
<CreatePostForm />
|
||||
<div className="h-[40vh] w-full max-w-2xl overflow-y-scroll">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<PostCardSkeleton />
|
||||
<PostCardSkeleton />
|
||||
<PostCardSkeleton />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PostList />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
66
apps/nextjs/src/app/providers.tsx
Normal file
66
apps/nextjs/src/app/providers.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { env } from "~/env.mjs";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||
if (env.VERCEL_URL) return env.VERCEL_URL; // SSR should use vercel url
|
||||
|
||||
return `http://localhost:${env.PORT}`; // dev SSR should use localhost
|
||||
};
|
||||
|
||||
export function TRPCReactProvider(props: {
|
||||
children: React.ReactNode;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
api.createClient({
|
||||
transformer: superjson,
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === "development" ||
|
||||
(opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
unstable_httpBatchStreamLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers() {
|
||||
const headers = new Map(props.headers);
|
||||
headers.set("x-trpc-source", "nextjs-react");
|
||||
return Object.fromEntries(headers);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<api.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration transformer={superjson}>
|
||||
{props.children}
|
||||
</ReactQueryStreamedHydration>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</api.Provider>
|
||||
);
|
||||
}
|
||||
45
apps/nextjs/src/env.mjs
Normal file
45
apps/nextjs/src/env.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
shared: {
|
||||
VERCEL_URL: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? `https://${v}` : undefined)),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
},
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
||||
* built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
DB_USERNAME: z.string(),
|
||||
DB_PASSWORD: z.string(),
|
||||
DB_HOST: z.string(),
|
||||
DB_NAME: z.string(),
|
||||
},
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
/**
|
||||
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
PORT: process.env.PORT,
|
||||
DB_USERNAME: process.env.DB_USERNAME,
|
||||
DB_PASSWORD: process.env.DB_PASSWORD,
|
||||
DB_HOST: process.env.DB_HOST,
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
},
|
||||
skipValidation:
|
||||
!!process.env.CI ||
|
||||
!!process.env.SKIP_ENV_VALIDATION ||
|
||||
process.env.npm_lifecycle_event === "lint",
|
||||
});
|
||||
0
apps/nextjs/src/styles/globals.css
Normal file
0
apps/nextjs/src/styles/globals.css
Normal file
7
apps/nextjs/src/utils/api.ts
Normal file
7
apps/nextjs/src/utils/api.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
import type { AppRouter } from "@acme/api";
|
||||
|
||||
export const api = createTRPCReact<AppRouter>();
|
||||
|
||||
export { type RouterInputs, type RouterOutputs } from "@acme/api";
|
||||
17
apps/nextjs/tsconfig.json
Normal file
17
apps/nextjs/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@acme/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": [".", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user