Got it running with sentry and plausible and stuff. Auth seems good
This commit is contained in:
26
src/app/globals.css
Normal file
26
src/app/globals.css
Normal 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
40
src/app/layout.tsx
Normal 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
159
src/app/page.tsx
Normal 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
31
src/app/server/inner.tsx
Normal 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
24
src/app/server/page.tsx
Normal 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
71
src/app/signin/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
19
src/components/ConvexClientProvider.tsx
Normal file
19
src/components/ConvexClientProvider.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
|
||||
import { ConvexReactClient } from 'convex/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
||||
|
||||
export default function ConvexClientProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ConvexAuthNextjsProvider client={convex}>
|
||||
{children}
|
||||
</ConvexAuthNextjsProvider>
|
||||
);
|
||||
}
|
38
src/env.js
Normal file
38
src/env.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
NODE_ENV: z.enum(['development', 'test', 'production'])
|
||||
.default('development'),
|
||||
SKIP_ENV_VALIDATION: z.boolean().default(false),
|
||||
CONVEX_SELF_HOSTED_URL: z.string(),
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: z.string(),
|
||||
SENTRY_AUTH_TOKEN: z.string(),
|
||||
CI: z.boolean().default(true),
|
||||
},
|
||||
client: {
|
||||
NEXT_PUBLIC_CONVEX_URL: z.url(),
|
||||
NEXT_PUBLIC_SITE_URL: z.url().default('http://localhost:3000'),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.url(),
|
||||
NEXT_PUBLIC_SENTRY_URL: z.url(),
|
||||
NEXT_PUBLIC_SENTRY_ORG: z.string().default('gib'),
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME: z.string(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
SKIP_ENV_VALIDATION: process.env.SKIP_ENV_VALIDATION,
|
||||
CONVEX_SELF_HOSTED_URL: process.env.CONVEX_SELF_HOSTED_URL,
|
||||
CONVEX_SELF_HOSTED_ADMIN_KEY: process.env.CONVEX_SELF_HOSTED_ADMIN_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
CI: process.env.CI,
|
||||
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
|
||||
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
|
||||
NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
|
||||
NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
|
||||
},
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
16
src/instrumentation-client.ts
Normal file
16
src/instrumentation-client.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN!,
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
|
||||
sendDefaultPii: true,
|
||||
// https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
|
||||
tracesSampleRate: 1.0,
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
// `captureRouterTransitionStart` is available from SDK version 9.12.0 onwards
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
10
src/instrumentation.ts
Normal file
10
src/instrumentation.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import type { Instrumentation } from 'next';
|
||||
|
||||
export const register = async () => {
|
||||
await import('../sentry.server.config');
|
||||
};
|
||||
|
||||
export const onRequestError: Instrumentation.onRequestError = (...args) => {
|
||||
Sentry.captureRequestError(...args);
|
||||
};
|
23
src/middleware.ts
Normal file
23
src/middleware.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
convexAuthNextjsMiddleware,
|
||||
createRouteMatcher,
|
||||
nextjsMiddlewareRedirect,
|
||||
} from '@convex-dev/auth/nextjs/server';
|
||||
|
||||
const isSignInPage = createRouteMatcher(['/signin']);
|
||||
const isProtectedRoute = createRouteMatcher(['/', '/server']);
|
||||
|
||||
export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
|
||||
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
|
||||
return nextjsMiddlewareRedirect(request, '/');
|
||||
}
|
||||
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
|
||||
return nextjsMiddlewareRedirect(request, '/signin');
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
// The following matcher runs middleware on all routes
|
||||
// except static assets.
|
||||
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
|
||||
};
|
Reference in New Issue
Block a user