Skip to content

Commit 7645fb7

Browse files
authored
Add auth frontend (#7)
1 parent b53e618 commit 7645fb7

35 files changed

+1701
-126
lines changed

project/apps/api-gateway/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
$ pnpm install
3333
```
3434

35+
Copy the `.env.example` file to create a new `.env` file:
36+
37+
```bash
38+
$ cp .env.example .env
39+
```
40+
41+
Modify the `.env` file with your environment-specific configuration.
42+
3543
## Compile and run the project
3644

3745
```bash

project/apps/api-gateway/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
"update-types": "npx supabase gen types --lang=typescript --project-id kamxbsekjfdzemvoevgz > src/supabase/database.types.ts"
2323
},
2424
"dependencies": {
25-
"@repo/pipes": "workspace:*",
26-
"@repo/dtos": "workspace:*",
2725
"@repo/eslint-config": "workspace:*",
2826
"@nestjs/common": "^10.0.0",
2927
"@nestjs/config": "^3.2.3",

project/apps/web/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000

project/apps/web/.gitignore

Lines changed: 0 additions & 36 deletions
This file was deleted.

project/apps/web/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ pnpm dev
1414
bun dev
1515
```
1616

17+
Copy the `.env.example` file to create a new `.env` file:
18+
19+
```bash
20+
$ cp .env.example .env
21+
```
22+
23+
Modify the `.env` file with your environment-specific configuration.
24+
1725
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
1826

1927
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

project/apps/web/app/auth/page.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardFooter,
10+
CardHeader,
11+
CardTitle,
12+
} from "@/components/ui/card";
13+
import { SignInForm } from "./signInForm";
14+
import { SignUpForm } from "./signUpForm";
15+
import { PublicPageWrapper } from "@/components/AuthWrappers/PublicPageWrapper";
16+
import { LANDING } from "@/lib/routes";
17+
18+
export default function AuthPage() {
19+
const [isSignUp, setIsSignUp] = useState(false);
20+
21+
return (
22+
<PublicPageWrapper redirect={{ strict: true, defaultUrl: LANDING }}>
23+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
24+
<Card className="w-full max-w-md">
25+
<CardHeader>
26+
<CardTitle>
27+
{isSignUp ? "Create an account" : "Sign in to your account"}
28+
</CardTitle>
29+
<CardDescription>
30+
{isSignUp
31+
? "Enter your details to create a new account"
32+
: "Enter your credentials to access your account"}
33+
</CardDescription>
34+
</CardHeader>
35+
<CardContent>
36+
{isSignUp ? <SignUpForm /> : <SignInForm />}{" "}
37+
<div className="relative my-4">
38+
<div className="absolute inset-0 flex items-center">
39+
<span className="w-full border-t" />
40+
</div>
41+
</div>
42+
</CardContent>
43+
<CardFooter>
44+
<Button
45+
variant="link"
46+
className="w-full"
47+
onClick={() => setIsSignUp(!isSignUp)}
48+
>
49+
{isSignUp
50+
? "Already have an account? Sign In"
51+
: "Don't have an account? Sign Up"}
52+
</Button>
53+
</CardFooter>
54+
</Card>
55+
</div>
56+
</PublicPageWrapper>
57+
);
58+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use client";
2+
3+
import { signInSchema, SignInDto } from "@repo/dtos/auth";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Form,
7+
FormControl,
8+
FormField,
9+
FormItem,
10+
FormLabel,
11+
FormMessage,
12+
} from "@/components/ui/form";
13+
import { Input } from "@/components/ui/input";
14+
import { useMutation, useQueryClient } from "@tanstack/react-query";
15+
import { signIn } from "@/lib/api/auth";
16+
import { useZodForm } from "@/lib/form";
17+
import { useLoginState } from "@/contexts/LoginStateContext";
18+
import { useToast } from "@/hooks/use-toast";
19+
20+
export function SignInForm() {
21+
const form = useZodForm({ schema: signInSchema });
22+
const { setHasLoginStateFlag } = useLoginState();
23+
const { toast } = useToast();
24+
const queryClient = useQueryClient();
25+
26+
const mutation = useMutation({
27+
mutationFn: signIn,
28+
onSuccess: async () => {
29+
setHasLoginStateFlag();
30+
await queryClient.invalidateQueries({ queryKey: ["me"] });
31+
},
32+
onError(error) {
33+
toast({
34+
description: error.message,
35+
variant: "destructive",
36+
});
37+
},
38+
});
39+
function onSubmit(values: SignInDto) {
40+
mutation.mutate(values);
41+
}
42+
return (
43+
<Form {...form}>
44+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
45+
<FormField
46+
control={form.control}
47+
name="email"
48+
render={({ field }) => (
49+
<FormItem>
50+
<FormLabel>Email</FormLabel>
51+
<FormControl>
52+
<Input {...field} />
53+
</FormControl>
54+
<FormMessage />
55+
</FormItem>
56+
)}
57+
/>
58+
<FormField
59+
control={form.control}
60+
name="password"
61+
render={({ field }) => (
62+
<FormItem>
63+
<FormLabel>Password</FormLabel>
64+
<FormControl>
65+
<Input type="password" {...field} />
66+
</FormControl>
67+
<FormMessage />
68+
</FormItem>
69+
)}
70+
/>
71+
<Button className="w-full" type="submit">
72+
Submit
73+
</Button>
74+
</form>
75+
</Form>
76+
);
77+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import { signUpSchema, SignUpDto } from "@repo/dtos/auth";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Form,
7+
FormControl,
8+
FormField,
9+
FormItem,
10+
FormLabel,
11+
FormMessage,
12+
} from "@/components/ui/form";
13+
import { Input } from "@/components/ui/input";
14+
import { useMutation, useQueryClient } from "@tanstack/react-query";
15+
import { signUp } from "@/lib/api/auth";
16+
import { useZodForm } from "@/lib/form";
17+
import { useLoginState } from "@/contexts/LoginStateContext";
18+
import { useToast } from "@/hooks/use-toast";
19+
20+
export function SignUpForm() {
21+
const form = useZodForm({ schema: signUpSchema });
22+
const { setHasLoginStateFlag } = useLoginState();
23+
const queryClient = useQueryClient();
24+
const { toast } = useToast();
25+
const mutation = useMutation({
26+
mutationFn: signUp,
27+
onSuccess: async () => {
28+
setHasLoginStateFlag();
29+
await queryClient.invalidateQueries({ queryKey: ["me"] });
30+
},
31+
onError: (error) => {
32+
toast({
33+
description: error.message,
34+
variant: "destructive",
35+
});
36+
},
37+
});
38+
function onSubmit(values: SignUpDto) {
39+
mutation.mutate(values);
40+
}
41+
return (
42+
<Form {...form}>
43+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
44+
<FormField
45+
control={form.control}
46+
name="username"
47+
render={({ field }) => (
48+
<FormItem>
49+
<FormLabel>Username</FormLabel>
50+
<FormControl>
51+
<Input {...field} />
52+
</FormControl>
53+
<FormMessage />
54+
</FormItem>
55+
)}
56+
/>
57+
<FormField
58+
control={form.control}
59+
name="email"
60+
render={({ field }) => (
61+
<FormItem>
62+
<FormLabel>Email</FormLabel>
63+
<FormControl>
64+
<Input {...field} />
65+
</FormControl>
66+
<FormMessage />
67+
</FormItem>
68+
)}
69+
/>
70+
<FormField
71+
control={form.control}
72+
name="password"
73+
render={({ field }) => (
74+
<FormItem>
75+
<FormLabel>Password</FormLabel>
76+
<FormControl>
77+
<Input type="password" {...field} />
78+
</FormControl>
79+
<FormMessage />
80+
</FormItem>
81+
)}
82+
/>
83+
<FormField
84+
control={form.control}
85+
name="confirmPassword"
86+
render={({ field }) => (
87+
<FormItem>
88+
<FormLabel>Confirm Password</FormLabel>
89+
<FormControl>
90+
<Input type="password" {...field} />
91+
</FormControl>
92+
<FormMessage />
93+
</FormItem>
94+
)}
95+
/>
96+
<Button className="w-full" type="submit">
97+
Submit
98+
</Button>
99+
</form>
100+
</Form>
101+
);
102+
}

project/apps/web/app/layout.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import ReactQueryProvider from "@/components/ReactQueryProvider";
2+
import Suspense from "@/components/Suspense";
3+
import { Skeleton } from "@/components/ui/skeleton";
4+
import { LoginStateProvider } from "@/contexts/LoginStateContext";
15
import "./globals.css";
6+
import { Toaster } from "@/components/ui/toaster";
27

38
export default function RootLayout({
49
children,
@@ -7,7 +12,14 @@ export default function RootLayout({
712
}>) {
813
return (
914
<html lang="en">
10-
<body>{children}</body>
15+
<body>
16+
<LoginStateProvider>
17+
<Suspense fallback={<Skeleton className="w-screen h-screen" />}>
18+
<ReactQueryProvider>{children}</ReactQueryProvider>
19+
<Toaster />
20+
</Suspense>
21+
</LoginStateProvider>
22+
</body>
1123
</html>
1224
);
1325
}

0 commit comments

Comments
 (0)