init commit

This commit is contained in:
2025-08-28 13:13:06 -05:00
commit 2f06340a63
32 changed files with 2568 additions and 0 deletions

26
app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

40
app/layout.tsx Normal file
View File

@@ -0,0 +1,40 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server";
import ConvexClientProvider from "@/components/ConvexClientProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
icons: {
icon: "/convex.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ConvexAuthNextjsServerProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
</ConvexAuthNextjsServerProvider>
);
}

159
app/page.tsx Normal file
View File

@@ -0,0 +1,159 @@
"use client";
import { useConvexAuth, useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import Link from "next/link";
import { useAuthActions } from "@convex-dev/auth/react";
import { useRouter } from "next/navigation";
export default function Home() {
return (
<>
<header className="sticky top-0 z-10 bg-background p-4 border-b-2 border-slate-200 dark:border-slate-800 flex flex-row justify-between items-center">
Convex + Next.js + Convex Auth
<SignOutButton />
</header>
<main className="p-8 flex flex-col gap-8">
<h1 className="text-4xl font-bold text-center">
Convex + Next.js + Convex Auth
</h1>
<Content />
</main>
</>
);
}
function SignOutButton() {
const { isAuthenticated } = useConvexAuth();
const { signOut } = useAuthActions();
const router = useRouter();
return (
<>
{isAuthenticated && (
<button
className="bg-slate-200 dark:bg-slate-800 text-foreground rounded-md px-2 py-1"
onClick={() =>
void signOut().then(() => {
router.push("/signin");
})
}
>
Sign out
</button>
)}
</>
);
}
function Content() {
const { viewer, numbers } =
useQuery(api.myFunctions.listNumbers, {
count: 10,
}) ?? {};
const addNumber = useMutation(api.myFunctions.addNumber);
if (viewer === undefined || numbers === undefined) {
return (
<div className="mx-auto">
<p>loading... (consider a loading skeleton)</p>
</div>
);
}
return (
<div className="flex flex-col gap-8 max-w-lg mx-auto">
<p>Welcome {viewer ?? "Anonymous"}!</p>
<p>
Click the button below and open this page in another window - this data
is persisted in the Convex cloud database!
</p>
<p>
<button
className="bg-foreground text-background text-sm px-4 py-2 rounded-md"
onClick={() => {
void addNumber({ value: Math.floor(Math.random() * 10) });
}}
>
Add a random number
</button>
</p>
<p>
Numbers:{" "}
{numbers?.length === 0
? "Click the button!"
: (numbers?.join(", ") ?? "...")}
</p>
<p>
Edit{" "}
<code className="text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md">
convex/myFunctions.ts
</code>{" "}
to change your backend
</p>
<p>
Edit{" "}
<code className="text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md">
app/page.tsx
</code>{" "}
to change your frontend
</p>
<p>
See the{" "}
<Link href="/server" className="underline hover:no-underline">
/server route
</Link>{" "}
for an example of loading data in a server component
</p>
<div className="flex flex-col">
<p className="text-lg font-bold">Useful resources:</p>
<div className="flex gap-2">
<div className="flex flex-col gap-2 w-1/2">
<ResourceCard
title="Convex docs"
description="Read comprehensive documentation for all Convex features."
href="https://docs.convex.dev/home"
/>
<ResourceCard
title="Stack articles"
description="Learn about best practices, use cases, and more from a growing
collection of articles, videos, and walkthroughs."
href="https://www.typescriptlang.org/docs/handbook/2/basic-types.html"
/>
</div>
<div className="flex flex-col gap-2 w-1/2">
<ResourceCard
title="Templates"
description="Browse our collection of templates to get started quickly."
href="https://www.convex.dev/templates"
/>
<ResourceCard
title="Discord"
description="Join our developer community to ask questions, trade tips & tricks,
and show off your projects."
href="https://www.convex.dev/community"
/>
</div>
</div>
</div>
</div>
);
}
function ResourceCard({
title,
description,
href,
}: {
title: string;
description: string;
href: string;
}) {
return (
<div className="flex flex-col gap-2 bg-slate-200 dark:bg-slate-800 p-4 rounded-md h-28 overflow-auto">
<a href={href} className="text-sm underline hover:no-underline">
{title}
</a>
<p className="text-xs">{description}</p>
</div>
);
}

31
app/server/inner.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client";
import { Preloaded, useMutation, usePreloadedQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
export default function Home({
preloaded,
}: {
preloaded: Preloaded<typeof api.myFunctions.listNumbers>;
}) {
const data = usePreloadedQuery(preloaded);
const addNumber = useMutation(api.myFunctions.addNumber);
return (
<>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">Reactive client-loaded data</h2>
<code>
<pre>{JSON.stringify(data, null, 2)}</pre>
</code>
</div>
<button
className="bg-foreground text-background px-4 py-2 rounded-md mx-auto"
onClick={() => {
void addNumber({ value: Math.floor(Math.random() * 10) });
}}
>
Add a random number
</button>
</>
);
}

24
app/server/page.tsx Normal file
View File

@@ -0,0 +1,24 @@
import Home from "./inner";
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
export default async function ServerPage() {
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
count: 3,
});
const data = preloadedQueryResult(preloaded);
return (
<main className="p-8 flex flex-col gap-4 mx-auto max-w-2xl">
<h1 className="text-4xl font-bold text-center">Convex + Next.js</h1>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">Non-reactive server-loaded data</h2>
<code>
<pre>{JSON.stringify(data, null, 2)}</pre>
</code>
</div>
<Home preloaded={preloaded} />
</main>
);
}

71
app/signin/page.tsx Normal file
View File

@@ -0,0 +1,71 @@
"use client";
import { useAuthActions } from "@convex-dev/auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function SignIn() {
const { signIn } = useAuthActions();
const [flow, setFlow] = useState<"signIn" | "signUp">("signIn");
const [error, setError] = useState<string | null>(null);
const router = useRouter();
return (
<div className="flex flex-col gap-8 w-96 mx-auto h-screen justify-center items-center">
<p>Log in to see the numbers</p>
<form
className="flex flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
formData.set("flow", flow);
void signIn("password", formData)
.catch((error) => {
setError(error.message);
})
.then(() => {
router.push("/");
});
}}
>
<input
className="bg-background text-foreground rounded-md p-2 border-2 border-slate-200 dark:border-slate-800"
type="email"
name="email"
placeholder="Email"
/>
<input
className="bg-background text-foreground rounded-md p-2 border-2 border-slate-200 dark:border-slate-800"
type="password"
name="password"
placeholder="Password"
/>
<button
className="bg-foreground text-background rounded-md"
type="submit"
>
{flow === "signIn" ? "Sign in" : "Sign up"}
</button>
<div className="flex flex-row gap-2">
<span>
{flow === "signIn"
? "Don't have an account?"
: "Already have an account?"}
</span>
<span
className="text-foreground underline hover:no-underline cursor-pointer"
onClick={() => setFlow(flow === "signIn" ? "signUp" : "signIn")}
>
{flow === "signIn" ? "Sign up instead" : "Sign in instead"}
</span>
</div>
{error && (
<div className="bg-red-500/20 border-2 border-red-500/50 rounded-md p-2">
<p className="text-foreground font-mono text-xs">
Error signing in: {error}
</p>
</div>
)}
</form>
</div>
);
}