Got it running with sentry and plausible and stuff. Auth seems good

This commit is contained in:
2025-08-28 15:28:28 -05:00
parent b672470bc4
commit 44d2ba3c5e
28 changed files with 1026 additions and 331 deletions

26
src/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
src/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
src/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
src/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
src/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
src/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>
);
}