Skip to content

Commit dd1d05b

Browse files
committed
create the trpc frontend
1 parent 07e236a commit dd1d05b

27 files changed

+582
-106
lines changed

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ install-dependencies:
55
pnpm add @tanstack/react-query@4.18.0
66
pnpm add @tanstack/react-query-devtools@4.18.0
77
pnpm add -D @tanstack/eslint-plugin-query
8-
9-
pnpm add @trpc/server
8+
pnpm add react-hook-form @hookform/resolvers
9+
pnpm add tailwind-merge
10+
pnpm add react-hot-toast
1011
pnpm add @trpc/client
1112
pnpm add @trpc/react-query
13+
14+
pnpm add @trpc/server
1215
pnpm add superjson
1316
pnpm add zod
1417
pnpm add jsonwebtoken
1518
pnpm add bcryptjs
1619
pnpm add -D @types/jsonwebtoken @types/bcryptjs
17-
1820
pnpm add @prisma/client
1921
pnpm add -D prisma
2022

app/api/trpc/[trpc]/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import {
2-
FetchCreateContextFnOptions,
3-
fetchRequestHandler,
4-
} from '@trpc/server/adapters/fetch';
1+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
52
import { appRouter } from '../trpc-router';
3+
import { createContext } from '@/utils/trpc-context';
64

75
const handler = (request: Request) => {
86
console.log(`incoming request ${request.url}`);
97
return fetchRequestHandler({
108
endpoint: '/api/trpc',
119
req: request,
1210
router: appRouter,
13-
createContext: function (
14-
opts: FetchCreateContextFnOptions
15-
): object | Promise<object> {
16-
return {};
17-
},
11+
createContext: createContext,
1812
});
1913
};
2014

app/api/trpc/trpc-router.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import authRouter from '@/server/auth-route';
22
import { getMeHandler } from '@/server/user-controller';
33
import { createContext } from '@/utils/trpc-context';
44
import { protectedProcedure, t } from '@/utils/trpc-server';
5-
import { createServerSideHelpers } from '@trpc/react-query/server';
6-
import SuperJSON from 'superjson';
75

86
const healthCheckerRouter = t.router({
97
healthchecker: t.procedure.query(() => {
@@ -24,11 +22,11 @@ export const appRouter = t.mergeRouters(
2422
userRouter
2523
);
2624

27-
export const createSSRHelper = () =>
28-
createServerSideHelpers({
29-
router: appRouter,
30-
transformer: SuperJSON,
31-
ctx: createContext,
32-
});
25+
export const createCaller = t.createCallerFactory(appRouter);
26+
27+
export const createAsyncCaller = async () => {
28+
const context = await createContext();
29+
return createCaller(context);
30+
};
3331

3432
export type AppRouter = typeof appRouter;

app/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
22
import { Inter } from 'next/font/google';
33
import './globals.css';
44
import { TrpcProvider } from '@/utils/trpc-provider';
5+
import { Toaster } from 'react-hot-toast';
56

67
const inter = Inter({ subsets: ['latin'] });
78

@@ -19,7 +20,10 @@ export default function RootLayout({
1920
<html lang='en'>
2021
<body className={inter.className}>
2122
<TrpcProvider>
22-
<div>{children}</div>
23+
<div>
24+
{children}
25+
<Toaster />
26+
</div>
2327
</TrpcProvider>
2428
</body>
2529
</html>

app/login/login-form.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
4+
import { zodResolver } from '@hookform/resolvers/zod';
5+
import { useState } from 'react';
6+
import Link from 'next/link';
7+
import { useRouter } from 'next/navigation';
8+
import { LoginUserInput, loginUserSchema } from '@/lib/user-schema';
9+
import FormInput from '@/components/form-input';
10+
import { LoadingButton } from '@/components/loading-button';
11+
import { trpc } from '@/utils/trpc';
12+
import toast from 'react-hot-toast';
13+
14+
export default function LoginForm() {
15+
const [submitting, setSubmitting] = useState(false);
16+
const router = useRouter();
17+
18+
const methods = useForm<LoginUserInput>({
19+
resolver: zodResolver(loginUserSchema),
20+
});
21+
22+
const { reset, handleSubmit } = methods;
23+
24+
const { mutate: loginFn } = trpc.loginUser.useMutation({
25+
onSettled() {
26+
setSubmitting(false);
27+
},
28+
onMutate() {
29+
setSubmitting(true);
30+
},
31+
onError(error) {
32+
toast.error(error.message);
33+
console.log('Error message:', error.message);
34+
reset({ password: '' });
35+
},
36+
onSuccess() {
37+
toast.success('login successfully');
38+
router.push('/');
39+
},
40+
});
41+
42+
const onSubmitHandler: SubmitHandler<LoginUserInput> = (values) => {
43+
loginFn(values);
44+
};
45+
46+
return (
47+
<FormProvider {...methods}>
48+
<form
49+
onSubmit={handleSubmit(onSubmitHandler)}
50+
className='max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5'
51+
>
52+
<FormInput label='Email' name='email' type='email' />
53+
<FormInput label='Password' name='password' type='password' />
54+
55+
<div className='text-right'>
56+
<Link href='#' className=''>
57+
Forgot Password?
58+
</Link>
59+
</div>
60+
<LoadingButton loading={submitting} textColor='text-ct-blue-600'>
61+
Login
62+
</LoadingButton>
63+
<span className='block'>
64+
Need an account?{' '}
65+
<Link href='/register' className='text-ct-blue-600'>
66+
Sign Up Here
67+
</Link>
68+
</span>
69+
</form>
70+
</FormProvider>
71+
);
72+
}

app/login/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Header from '@/components/header';
2+
import LoginForm from './login-form';
3+
4+
export default async function LoginPage() {
5+
return (
6+
<>
7+
<Header />
8+
<section className='bg-ct-blue-600 min-h-screen grid place-items-center'>
9+
<div className='w-full'>
10+
<h1 className='text-4xl lg:text-6xl text-center font-[600] text-ct-yellow-600 mb-4'>
11+
Welcome Back
12+
</h1>
13+
<h2 className='text-lg text-center mb-4 text-ct-dark-200'>
14+
Login to have access
15+
</h2>
16+
<LoginForm />
17+
</div>
18+
</section>
19+
</>
20+
);
21+
}

app/page.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
'use client';
2-
3-
import { trpc } from '@/utils/trpc';
1+
import Header from '@/components/header';
42

53
export default function Home() {
6-
let { data, isLoading, isFetching } = trpc.healthchecker.useQuery();
7-
if (isLoading || isFetching) return <p>Loading...</p>;
8-
94
return (
10-
<div className='text-xl font-bold'>
11-
<h1>Status: {data?.status}</h1>
12-
<h1>Message: {data?.message}</h1>
13-
</div>
5+
<>
6+
<Header />
7+
<section className='bg-ct-blue-600 min-h-screen pt-20'>
8+
<div className='max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center'>
9+
<p className='text-3xl font-semibold'>
10+
Implement Authentication with tRPC in Next.js 14
11+
</p>
12+
</div>
13+
</section>
14+
</>
1415
);
1516
}

app/profile/page.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Header from '@/components/header';
2+
import { getAuthUser } from '@/utils/get-auth-user';
3+
4+
export default async function ProfilePage() {
5+
const user = await getAuthUser();
6+
7+
return (
8+
<>
9+
<Header />
10+
<section className='bg-ct-blue-600 min-h-screen pt-20'>
11+
<div className='max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center'>
12+
<div>
13+
<p className='mb-3 text-5xl text-center font-semibold'>
14+
Profile Page
15+
</p>
16+
<div className='mt-8'>
17+
<p className='mb-3'>Id: {user?.id}</p>
18+
<p className='mb-3'>Name: {user?.name}</p>
19+
<p className='mb-3'>Email: {user?.email}</p>
20+
<p className='mb-3'>Role: {user?.role}</p>
21+
<p className='mb-3'>Verified: {String(user?.verified)}</p>
22+
</div>
23+
</div>
24+
</div>
25+
</section>
26+
</>
27+
);
28+
}

app/register/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Header from '@/components/header';
2+
import RegisterForm from './register-form';
3+
4+
export default async function RegisterPage() {
5+
return (
6+
<>
7+
<Header />
8+
<section className='py-8 bg-ct-blue-600 min-h-screen grid place-items-center'>
9+
<div className='w-full'>
10+
<h1 className='text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4'>
11+
Welcome to CodevoWeb!
12+
</h1>
13+
<h2 className='text-lg text-center mb-4 text-ct-dark-200'>
14+
Sign Up To Get Started!
15+
</h2>
16+
<RegisterForm />
17+
</div>
18+
</section>
19+
</>
20+
);
21+
}

app/register/register-form.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
4+
import { zodResolver } from '@hookform/resolvers/zod';
5+
import { useState } from 'react';
6+
import Link from 'next/link';
7+
import { toast } from 'react-hot-toast';
8+
import { useRouter } from 'next/navigation';
9+
import { CreateUserInput, createUserSchema } from '@/lib/user-schema';
10+
import { trpc } from '@/utils/trpc';
11+
import FormInput from '@/components/form-input';
12+
import { LoadingButton } from '@/components/loading-button';
13+
14+
export default function RegisterForm() {
15+
const router = useRouter();
16+
const [submitting, setSubmitting] = useState(false);
17+
18+
const methods = useForm<CreateUserInput>({
19+
resolver: zodResolver(createUserSchema),
20+
});
21+
22+
const { reset, handleSubmit } = methods;
23+
24+
const { mutate: registerFn } = trpc.registerUser.useMutation({
25+
onMutate() {
26+
setSubmitting(true);
27+
},
28+
onSettled() {
29+
setSubmitting(false);
30+
},
31+
onError(error) {
32+
reset({ password: '', passwordConfirm: '' });
33+
toast.error(error.message);
34+
console.log('Error message:', error.message);
35+
},
36+
onSuccess() {
37+
toast.success('registered successfully');
38+
router.push('/login');
39+
},
40+
});
41+
42+
const onSubmitHandler: SubmitHandler<CreateUserInput> = (values) => {
43+
registerFn(values);
44+
};
45+
46+
return (
47+
<FormProvider {...methods}>
48+
<form
49+
onSubmit={handleSubmit(onSubmitHandler)}
50+
className='max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5'
51+
>
52+
<FormInput label='Full Name' name='name' />
53+
<FormInput label='Email' name='email' type='email' />
54+
<FormInput label='Password' name='password' type='password' />
55+
<FormInput
56+
label='Confirm Password'
57+
name='passwordConfirm'
58+
type='password'
59+
/>
60+
<span className='block'>
61+
Already have an account?{' '}
62+
<Link href='/login' className='text-ct-blue-600'>
63+
Login Here
64+
</Link>
65+
</span>
66+
<LoadingButton loading={submitting} textColor='text-ct-blue-600'>
67+
Register
68+
</LoadingButton>
69+
</form>
70+
</FormProvider>
71+
);
72+
}

components/auth-menu.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import queryClient from '@/utils/query-client';
4+
import { trpc } from '@/utils/trpc';
5+
import Link from 'next/link';
6+
import { useRouter } from 'next/navigation';
7+
import toast from 'react-hot-toast';
8+
9+
export default function AuthMenu() {
10+
const router = useRouter();
11+
12+
const { mutate: logoutFn } = trpc.logoutUser.useMutation({
13+
onError(error) {
14+
toast.error(error.message);
15+
console.log('Error message:', error.message);
16+
},
17+
onSuccess() {
18+
queryClient.clear();
19+
toast.success('logout successful');
20+
router.push('/login');
21+
},
22+
});
23+
24+
return (
25+
<>
26+
<li>
27+
<Link href='/profile' className='text-ct-dark-600'>
28+
Profile
29+
</Link>
30+
</li>
31+
<li className='cursor-pointer' onClick={() => logoutFn()}>
32+
Logout
33+
</li>
34+
</>
35+
);
36+
}

0 commit comments

Comments
 (0)