Skip to content

Commit 594c6d7

Browse files
committed
feat: added trpc
1 parent 6ef8db5 commit 594c6d7

File tree

13 files changed

+221
-69
lines changed

13 files changed

+221
-69
lines changed

bun.lock

Lines changed: 44 additions & 33 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
"@radix-ui/react-navigation-menu": "^1.2.6",
2525
"@radix-ui/react-slot": "^1.2.0",
2626
"@radix-ui/react-tooltip": "^1.2.0",
27-
"@sentry/browser": "^9.12.0",
28-
"@sentry/tanstackstart-react": "^9.12.0",
29-
"@tanstack/react-form": "^1.4.0",
27+
"@sentry/browser": "^9.13.0",
28+
"@sentry/tanstackstart-react": "^9.13.0",
29+
"@tanstack/react-form": "^1.5.0",
3030
"@tanstack/react-query": "^5.74.3",
3131
"@tanstack/react-query-devtools": "^5.74.3",
3232
"@tanstack/react-router": "^1.116.0",
3333
"@tanstack/react-router-devtools": "^1.116.0",
3434
"@tanstack/react-router-with-query": "^1.116.0",
3535
"@tanstack/react-start": "^1.116.1",
36+
"@trpc/client": "^11.1.0",
37+
"@trpc/tanstack-react-query": "^11.1.0",
3638
"@vercel/analytics": "^1.5.0",
3739
"@vercel/speed-insights": "^1.2.0",
3840
"@vitejs/plugin-react": "^4.4.0",
@@ -59,6 +61,7 @@
5961
"simple-icons": "^13.21.0",
6062
"sitemap": "^8.0.0",
6163
"sonner": "^2.0.3",
64+
"superjson": "^2.2.2",
6265
"tailwind-merge": "^3.2.0",
6366
"tw-animate-css": "^1.2.5",
6467
"unplugin-fonts": "^1.3.1",
@@ -68,7 +71,7 @@
6871
"devDependencies": {
6972
"@commitlint/cli": "^19.8.0",
7073
"@commitlint/config-conventional": "^19.8.0",
71-
"@eslint-react/eslint-plugin": "^1.47.4",
74+
"@eslint-react/eslint-plugin": "^1.48.2",
7275
"@eslint/js": "^9.24.0",
7376
"@sentry/vite-plugin": "^3.3.1",
7477
"@tailwindcss/vite": "^4.1.4",
@@ -100,4 +103,4 @@
100103
"bun format --"
101104
]
102105
}
103-
}
106+
}

src/lib/server/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { betterAuth } from "better-auth";
22
import { drizzleAdapter } from "better-auth/adapters/drizzle";
3+
import { reactStartCookies } from "better-auth/react-start";
34

45
import { db } from "./db";
56

@@ -9,6 +10,9 @@ export const auth = betterAuth({
910
provider: "pg",
1011
}),
1112

13+
// https://www.better-auth.com/docs/integrations/tanstack#usage-tips
14+
plugins: [reactStartCookies()],
15+
1216
// https://www.better-auth.com/docs/concepts/session-management#session-caching
1317
session: {
1418
cookieCache: {

src/lib/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@ export function formatDate(input: string | number): string {
1313
year: "numeric",
1414
});
1515
}
16+
17+
export function getUrl() {
18+
const base = (() => {
19+
if (typeof window !== "undefined") return "";
20+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
21+
22+
return `http://localhost:${process.env.PORT ?? 3000}`;
23+
})();
24+
25+
return base + "/api/trpc";
26+
}

src/router.tsx

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,80 @@
11
import { QueryClient } from "@tanstack/react-query";
22
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
33
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
4+
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
5+
import superjson from "superjson";
46

57
import { DefaultCatchBoundary } from "~/lib/components/default-catch-boundary";
68
import { NotFound } from "~/lib/components/not-found";
9+
10+
import { createServerFn } from "@tanstack/react-start";
11+
import { getWebRequest } from "@tanstack/react-start/server";
12+
import { createTRPCClient, httpBatchStreamLink, loggerLink } from "@trpc/client";
13+
import { getUrl } from "~/lib/utils";
14+
import { TRPCProvider } from "~/trpc/react";
15+
import { AppRouter } from "~/trpc/router";
716
import { routeTree } from "./routeTree.gen";
817

18+
const getRequestHeaders = createServerFn({ method: "GET" }).handler(async () => {
19+
const request = getWebRequest()!;
20+
const headers = new Headers(request.headers);
21+
22+
return Object.fromEntries(headers);
23+
});
24+
925
export function createRouter() {
1026
const queryClient = new QueryClient({
1127
defaultOptions: {
1228
queries: {
1329
refetchOnWindowFocus: false,
1430
staleTime: 1000 * 60, // 1 minute
1531
},
32+
dehydrate: { serializeData: superjson.serialize },
33+
hydrate: { deserializeData: superjson.deserialize },
1634
},
1735
});
1836

19-
return routerWithQueryClient(
20-
createTanStackRouter({
21-
routeTree,
22-
context: { queryClient, user: null },
23-
defaultPreload: "intent",
24-
// react-query will handle data fetching & caching
25-
// https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#passing-all-loader-events-to-an-external-cache
26-
defaultPreloadStaleTime: 0,
27-
defaultErrorComponent: DefaultCatchBoundary,
28-
defaultNotFoundComponent: NotFound,
29-
scrollRestoration: true,
30-
defaultStructuralSharing: true,
31-
}),
37+
const trpcClient = createTRPCClient<AppRouter>({
38+
links: [
39+
loggerLink({
40+
enabled: (op) =>
41+
process.env.NODE_ENV === "development" ||
42+
(op.direction === "down" && op.result instanceof Error),
43+
}),
44+
httpBatchStreamLink({
45+
transformer: superjson,
46+
url: getUrl(),
47+
async headers() {
48+
return await getRequestHeaders();
49+
},
50+
}),
51+
],
52+
});
53+
54+
const trpc = createTRPCOptionsProxy<AppRouter>({
55+
client: trpcClient,
3256
queryClient,
33-
);
57+
});
58+
59+
const router = createTanStackRouter({
60+
context: { queryClient, trpc, user: null },
61+
routeTree,
62+
defaultPreload: "intent",
63+
defaultPreloadStaleTime: 0,
64+
defaultErrorComponent: DefaultCatchBoundary,
65+
defaultNotFoundComponent: NotFound,
66+
scrollRestoration: true,
67+
defaultStructuralSharing: true,
68+
Wrap: (props) => {
69+
return (
70+
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
71+
{props.children}
72+
</TRPCProvider>
73+
);
74+
},
75+
});
76+
77+
return routerWithQueryClient(router, queryClient);
3478
}
3579

3680
declare module "@tanstack/react-router" {

src/routes/__root.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@tanstack/react-router";
99
import { createServerFn } from "@tanstack/react-start";
1010
import { getWebRequest } from "@tanstack/react-start/server";
11+
import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query";
1112
import { Analytics } from "@vercel/analytics/react";
1213
import { SpeedInsights } from "@vercel/speed-insights/react";
1314

@@ -19,6 +20,7 @@ import { Toaster } from "~/lib/components/ui/sonner";
1920
import { ThemeProvider } from "~/lib/components/ui/theme";
2021
import { auth } from "~/lib/server/auth";
2122
import appCss from "~/lib/styles/app.css?url";
23+
import type { AppRouter } from "../../trpc-server.handler";
2224

2325
import "unfonts.css";
2426

@@ -31,6 +33,7 @@ const getUser = createServerFn({ method: "GET" }).handler(async () => {
3133

3234
export const Route = wrapCreateRootRouteWithSentry(createRootRouteWithContext)<{
3335
queryClient: QueryClient;
36+
trpc: TRPCOptionsProxy<AppRouter>;
3437
user: Awaited<ReturnType<typeof getUser>>;
3538
}>()({
3639
beforeLoad: async ({ context }) => {
File renamed without changes.

src/routes/api/trpc.$.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createAPIFileRoute } from "@tanstack/react-start/api";
2+
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3+
4+
import { createTRPCContext } from "~/trpc/init";
5+
import { appRouter } from "~/trpc/router";
6+
7+
function handler({ request }: { request: Request }) {
8+
return fetchRequestHandler({
9+
req: request,
10+
router: appRouter,
11+
endpoint: "/api/trpc",
12+
createContext: () => createTRPCContext(request),
13+
});
14+
}
15+
16+
export const APIRoute = createAPIFileRoute("/api/trpc/$")({
17+
GET: handler,
18+
POST: handler,
19+
});

src/routes/signin.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { createFileRoute, redirect } from "@tanstack/react-router";
22
import type { ComponentProps } from "react";
3+
import type { SimpleIcon } from "simple-icons";
4+
import { siGithub } from "simple-icons";
35
import authClient from "~/lib/auth-client";
46
import { Button } from "~/lib/components/ui/button";
7+
import Icon from "~/lib/components/ui/icon";
58
import { cn } from "~/lib/utils";
69

710
const REDIRECT_URL = "/dashboard";
@@ -19,24 +22,15 @@ export const Route = createFileRoute("/signin")({
1922

2023
function AuthPage() {
2124
return (
22-
<div className="flex min-h-screen items-center justify-center">
23-
<div className="flex flex-col items-center gap-8 rounded-xl border bg-card p-10">
24-
Logo here
25-
<div className="flex flex-col gap-2">
26-
<SignInButton
27-
provider="discord"
28-
label="Discord"
29-
className="bg-[#5865F2] hover:bg-[#5865F2]/80"
30-
/>
25+
<div className="flex min-h-[calc(100vh-200px)] items-center justify-center">
26+
<div className="flex w-full max-w-md flex-col items-center gap-8 rounded-xl border bg-card p-10 shadow-sm">
27+
<h1 className="text-2xl font-semibold">Sign in</h1>
28+
<div className="flex w-full flex-col gap-3">
3129
<SignInButton
3230
provider="github"
3331
label="GitHub"
34-
className="bg-neutral-700 hover:bg-neutral-700/80"
35-
/>
36-
<SignInButton
37-
provider="google"
38-
label="Google"
39-
className="bg-[#DB4437] hover:bg-[#DB4437]/80"
32+
icon={siGithub}
33+
className="bg-neutral-800 hover:bg-neutral-800/90"
4034
/>
4135
</div>
4236
</div>
@@ -47,11 +41,13 @@ function AuthPage() {
4741
interface SignInButtonProps extends ComponentProps<typeof Button> {
4842
provider: "discord" | "google" | "github";
4943
label: string;
44+
icon: SimpleIcon;
5045
}
5146

5247
function SignInButton({
5348
provider,
5449
label,
50+
icon,
5551
className,
5652
...props
5753
}: Readonly<SignInButtonProps>) {
@@ -65,10 +61,14 @@ function SignInButton({
6561
}
6662
type="button"
6763
size="lg"
68-
className={cn("text-white hover:text-white", className)}
64+
className={cn(
65+
"flex w-full items-center justify-center gap-2 text-white hover:text-white",
66+
className,
67+
)}
6968
{...props}
7069
>
71-
Sign in with {label}
70+
<Icon icon={icon} className="h-5 w-5" />
71+
<span>Sign in with {label}</span>
7272
</Button>
7373
);
7474
}

src/trpc/init.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { initTRPC, TRPCError } from "@trpc/server";
2+
import superjson from "superjson";
3+
4+
import { auth } from "~/lib/server/auth";
5+
import { db } from "~/lib/server/db";
6+
7+
export const createTRPCContext = async ({ headers }: { headers: Headers }) => {
8+
const session = await auth.api.getSession({
9+
headers,
10+
});
11+
12+
return {
13+
db,
14+
session,
15+
};
16+
};
17+
18+
export const t = initTRPC.context<typeof createTRPCContext>().create({
19+
transformer: superjson,
20+
});
21+
22+
export const createTRPCRouter = t.router;
23+
24+
const enforceUserIsAuthenticated = t.middleware(({ ctx, next }) => {
25+
if (!ctx.session?.user) {
26+
throw new TRPCError({ code: "UNAUTHORIZED" });
27+
}
28+
29+
return next({
30+
ctx: {
31+
session: { ...ctx.session, user: ctx.session.user },
32+
},
33+
});
34+
});
35+
36+
export const publicProcedure = t.procedure;
37+
export const protectedProcedure = t.procedure.use(enforceUserIsAuthenticated);

0 commit comments

Comments
 (0)