Initial commit
This commit is contained in:
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";
|
||||
Reference in New Issue
Block a user