Skip to content

Commit ef09a93

Browse files
feat: setup authentication
1 parent 37e1814 commit ef09a93

File tree

15 files changed

+655
-17
lines changed

15 files changed

+655
-17
lines changed

app/(auth)/layout.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1+
import Image from "next/image";
12
import React from "react";
23

4+
import SocialAuthForm from "@/components/forms/SocialAuthForm";
5+
import Theme from "@/components/navigation/navbar/Theme";
6+
37
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
4-
return <main>{children}</main>;
8+
return (
9+
<main className="flex min-h-screen items-center justify-center bg-auth-light bg-cover bg-center bg-no-repeat px-4 py-10 dark:bg-auth-dark">
10+
<section className="light-border background-light900_dark200 shadow-light100_dark100 min-w-full rounded-[10px] border px-4 py-10 shadow-md sm:min-w-[520px] sm:px-8">
11+
<div className="flex items-center justify-between gap-2">
12+
<div className="space-y-2.5">
13+
<h1 className="h2-bold text-dark100_light900">Join AITree</h1>
14+
<p className="paragraph-regular text-dark500_light400">
15+
To get started, please enter your details.
16+
</p>
17+
</div>
18+
<Image
19+
src="/images/site-logo.svg"
20+
alt="AI Tree"
21+
width={50}
22+
height={50}
23+
className="object-contain"
24+
/>
25+
</div>
26+
{children}
27+
28+
<SocialAuthForm />
29+
</section>
30+
31+
<Theme className="absolute right-10 top-10" />
32+
</main>
33+
);
534
};
635

736
export default AuthLayout;

app/(root)/page.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1-
const Home = () => {
1+
import { auth, signOut } from "@/auth";
2+
import { Button } from "@/components/ui/button";
3+
import ROUTES from "@/constants/routes";
4+
5+
const Home = async () => {
6+
const session = await auth();
7+
8+
console.log(session);
29
return (
310
<div className="flex h-screen flex-col items-center justify-center">
411
<h1 className="text-7xl font-bold">Welcome to the tree🌳</h1>
512
<h1 className="font-space-grotesk text-7xl font-bold">
613
Welcome to the tree 🌳
714
</h1>
15+
16+
<form
17+
className="mt-10"
18+
action={async () => {
19+
"use server";
20+
await signOut({ redirectTo: ROUTES.SIGN_IN });
21+
}}
22+
>
23+
<Button type="submit">Log out</Button>
24+
</form>
825
</div>
926
);
1027
};

app/api/auth/[...nextauth]/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { handlers } from "@/auth"; // Referring to the auth.ts we just created
2+
export const { GET, POST } = handlers;

app/layout.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { Metadata } from "next";
2+
import { SessionProvider } from "next-auth/react";
23
import { Inter, Space_Grotesk as SpaceGrotesk } from "next/font/google";
34

5+
import { auth } from "@/auth";
6+
import { Toaster } from "@/components/ui/toaster";
47
import ThemeProvider from "@/context/Theme";
58
import "./globals.css";
69

@@ -25,25 +28,29 @@ export const metadata: Metadata = {
2528
},
2629
};
2730

28-
export default function RootLayout({
31+
export default async function RootLayout({
2932
children,
3033
}: Readonly<{
3134
children: React.ReactNode;
3235
}>) {
36+
const session = await auth();
3337
return (
3438
<html suppressHydrationWarning lang="en">
35-
<body
36-
className={`${inter.className} ${spaceGrotesk.variable} antialiased`}
37-
>
38-
<ThemeProvider
39-
attribute="class"
40-
defaultTheme="dark"
41-
enableSystem
42-
disableTransitionOnChange
39+
<SessionProvider session={session}>
40+
<body
41+
className={`${inter.className} ${spaceGrotesk.variable} antialiased`}
4342
>
44-
{children}
45-
</ThemeProvider>
46-
</body>
43+
<ThemeProvider
44+
attribute="class"
45+
defaultTheme="dark"
46+
enableSystem
47+
disableTransitionOnChange
48+
>
49+
{children}
50+
</ThemeProvider>
51+
<Toaster />
52+
</body>
53+
</SessionProvider>
4754
</html>
4855
);
4956
}

auth.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import NextAuth from "next-auth";
2+
import GitHub from "next-auth/providers/github";
3+
import Google from "next-auth/providers/google";
4+
5+
export const { handlers, signIn, signOut, auth } = NextAuth({
6+
providers: [GitHub, Google],
7+
});

components/forms/SocialAuthForm.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
import { signIn } from "next-auth/react";
3+
import Image from "next/image";
4+
5+
import ROUTES from "@/constants/routes";
6+
import { toast } from "@/hooks/use-toast";
7+
8+
import { Button } from "../ui/button";
9+
10+
const SocialAuthForm = () => {
11+
const handleSignIn = async (provider: "github" | "google") => {
12+
try {
13+
await signIn(provider, { callbackUrl: ROUTES.HOME, redirect: false });
14+
} catch (error) {
15+
console.log(error);
16+
toast({
17+
title: "Sign in failed",
18+
description:
19+
error instanceof Error
20+
? error.message
21+
: "An error occurred during sign in",
22+
variant: "destructive",
23+
});
24+
}
25+
};
26+
27+
const buttonClass =
28+
"background-dark400_light900 body-medium text-dark200_light800 min-h-12 flex-1 rounded-2 px-4 py-3.5";
29+
return (
30+
<div className="mt-10 flex flex-wrap gap-2.5">
31+
<Button className={buttonClass} onClick={() => handleSignIn("github")}>
32+
<Image
33+
className="invert-colors mr-2.5 object-contain"
34+
src="/icons/github.svg"
35+
alt="github"
36+
width={20}
37+
height={20}
38+
/>
39+
<span>Log in with Github</span>
40+
</Button>
41+
<Button className={buttonClass} onClick={() => handleSignIn("google")}>
42+
<Image
43+
className="mr-2.5 object-contain"
44+
src="/icons/google.svg"
45+
alt="google"
46+
width={20}
47+
height={20}
48+
/>
49+
<span>Log in with Google</span>
50+
</Button>
51+
</div>
52+
);
53+
};
54+
55+
export default SocialAuthForm;

components/navigation/navbar/Theme.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import {
1111
DropdownMenuTrigger,
1212
} from "@/components/ui/dropdown-menu";
1313

14-
const Theme = () => {
14+
const Theme = ({ className }: { className?: string }) => {
1515
const { setTheme } = useTheme();
1616

1717
return (
1818
<DropdownMenu>
1919
<DropdownMenuTrigger asChild>
20-
<Button variant="outline" size="icon">
20+
<Button variant="outline" size="icon" className={className}>
2121
<Sun className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
2222
<Moon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
2323
<span className="sr-only">Toggle theme</span>

components/ui/toast.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as ToastPrimitives from "@radix-ui/react-toast"
5+
import { cva, type VariantProps } from "class-variance-authority"
6+
import { X } from "lucide-react"
7+
8+
import { cn } from "@/lib/utils"
9+
10+
const ToastProvider = ToastPrimitives.Provider
11+
12+
const ToastViewport = React.forwardRef<
13+
React.ElementRef<typeof ToastPrimitives.Viewport>,
14+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
15+
>(({ className, ...props }, ref) => (
16+
<ToastPrimitives.Viewport
17+
ref={ref}
18+
className={cn(
19+
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
20+
className
21+
)}
22+
{...props}
23+
/>
24+
))
25+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26+
27+
const toastVariants = cva(
28+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-slate-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-slate-800",
29+
{
30+
variants: {
31+
variant: {
32+
default: "border bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
33+
destructive:
34+
"destructive group border-red-500 bg-red-500 text-slate-50 dark:border-red-900 dark:bg-red-900 dark:text-slate-50",
35+
},
36+
},
37+
defaultVariants: {
38+
variant: "default",
39+
},
40+
}
41+
)
42+
43+
const Toast = React.forwardRef<
44+
React.ElementRef<typeof ToastPrimitives.Root>,
45+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
46+
VariantProps<typeof toastVariants>
47+
>(({ className, variant, ...props }, ref) => {
48+
return (
49+
<ToastPrimitives.Root
50+
ref={ref}
51+
className={cn(toastVariants({ variant }), className)}
52+
{...props}
53+
/>
54+
)
55+
})
56+
Toast.displayName = ToastPrimitives.Root.displayName
57+
58+
const ToastAction = React.forwardRef<
59+
React.ElementRef<typeof ToastPrimitives.Action>,
60+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
61+
>(({ className, ...props }, ref) => (
62+
<ToastPrimitives.Action
63+
ref={ref}
64+
className={cn(
65+
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-1 focus:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-slate-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-slate-50 group-[.destructive]:focus:ring-red-500 dark:border-slate-800 dark:hover:bg-slate-800 dark:focus:ring-slate-300 dark:group-[.destructive]:border-slate-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-slate-50 dark:group-[.destructive]:focus:ring-red-900",
66+
className
67+
)}
68+
{...props}
69+
/>
70+
))
71+
ToastAction.displayName = ToastPrimitives.Action.displayName
72+
73+
const ToastClose = React.forwardRef<
74+
React.ElementRef<typeof ToastPrimitives.Close>,
75+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
76+
>(({ className, ...props }, ref) => (
77+
<ToastPrimitives.Close
78+
ref={ref}
79+
className={cn(
80+
"absolute right-1 top-1 rounded-md p-1 text-slate-950/50 opacity-0 transition-opacity hover:text-slate-950 focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-slate-50/50 dark:hover:text-slate-50",
81+
className
82+
)}
83+
toast-close=""
84+
{...props}
85+
>
86+
<X className="h-4 w-4" />
87+
</ToastPrimitives.Close>
88+
))
89+
ToastClose.displayName = ToastPrimitives.Close.displayName
90+
91+
const ToastTitle = React.forwardRef<
92+
React.ElementRef<typeof ToastPrimitives.Title>,
93+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
94+
>(({ className, ...props }, ref) => (
95+
<ToastPrimitives.Title
96+
ref={ref}
97+
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
98+
{...props}
99+
/>
100+
))
101+
ToastTitle.displayName = ToastPrimitives.Title.displayName
102+
103+
const ToastDescription = React.forwardRef<
104+
React.ElementRef<typeof ToastPrimitives.Description>,
105+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
106+
>(({ className, ...props }, ref) => (
107+
<ToastPrimitives.Description
108+
ref={ref}
109+
className={cn("text-sm opacity-90", className)}
110+
{...props}
111+
/>
112+
))
113+
ToastDescription.displayName = ToastPrimitives.Description.displayName
114+
115+
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
116+
117+
type ToastActionElement = React.ReactElement<typeof ToastAction>
118+
119+
export {
120+
type ToastProps,
121+
type ToastActionElement,
122+
ToastProvider,
123+
ToastViewport,
124+
Toast,
125+
ToastTitle,
126+
ToastDescription,
127+
ToastClose,
128+
ToastAction,
129+
}

components/ui/toaster.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client"
2+
3+
import { useToast } from "@/hooks/use-toast"
4+
import {
5+
Toast,
6+
ToastClose,
7+
ToastDescription,
8+
ToastProvider,
9+
ToastTitle,
10+
ToastViewport,
11+
} from "@/components/ui/toast"
12+
13+
export function Toaster() {
14+
const { toasts } = useToast()
15+
16+
return (
17+
<ToastProvider>
18+
{toasts.map(function ({ id, title, description, action, ...props }) {
19+
return (
20+
<Toast key={id} {...props}>
21+
<div className="grid gap-1">
22+
{title && <ToastTitle>{title}</ToastTitle>}
23+
{description && (
24+
<ToastDescription>{description}</ToastDescription>
25+
)}
26+
</div>
27+
{action}
28+
<ToastClose />
29+
</Toast>
30+
)
31+
})}
32+
<ToastViewport />
33+
</ToastProvider>
34+
)
35+
}

constants/routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const ROUTES = {
2+
HOME: "/",
3+
SIGN_IN: "/sign-in",
4+
SIGN_UP: "/sign-up",
5+
};
6+
7+
export default ROUTES;

0 commit comments

Comments
 (0)