diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts index da9643720d846c9d78fe76977a9617cba3b0eb40..9aceda98f6bc309f010ed1a97c9264e7b0c4454b 100644 --- a/app/api/signup/route.ts +++ b/app/api/signup/route.ts @@ -3,21 +3,23 @@ import { hash } from 'bcrypt' import { NextResponse } from 'next/server' export async function POST(req: Request) { + console.log("aaaa", req) + try { const { username, email, password } = await req.json() const hashed = await hash(password, 12) const user = await db.user.create({ data: { - username, - email, + name: username, + username: username.toLowerCase(), + email: email.toLowerCase(), password: hashed } }) return NextResponse.json({ - username: user.username, - email: user.email + usernameOrEmail: user.email }) } catch (err: any) { return new NextResponse(JSON.stringify({ @@ -25,4 +27,19 @@ export async function POST(req: Request) { }), { status: 500 } ) } -} \ No newline at end of file +} + +// let isUnique = false; +// while (!isUnique) { +// const existingUserName = await db.user.findUnique({ +// where: { +// username: credentials.username +// } +// }) + +// if (existingUserName) { +// credentials.username = `${credentials.username}${Math.floor(Math.random() * 1000)}` +// } else { +// isUnique = true; +// } +// } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index f53ce33d37634e5395ba53be289a9e2a40f0659a..421584b8612589dd76b986cd3b0c7577f2f5fe0d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import './globals.css' import Providers from '@/components/react-query/provider' import SiteLoad from '@/components/site-loading' import { ThemeProvider } from '@/components/ui/theme-provider' +import { Toaster } from '@/components/ui/toaster' import { Suspense } from 'react' const inter = Inter({ subsets: ['latin'] }) @@ -26,6 +27,7 @@ export default function RootLayout({ <Suspense fallback={<SiteLoad />}> <Providers> {children} + <Toaster /> </Providers> </Suspense> </ThemeProvider> diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index 2deb2e9cc94af0be722b6a433a6d309621f12a5f..f8205889e631bae2ddc6941056bc5f39c13ccb8b 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -1,6 +1,6 @@ import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast" -import { VariantProps, cva } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority" import { X } from "lucide-react" import { cn } from "@/lib/utils" diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ac9370c3ca8b43935a190b0cc2df6fb1823a570a --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + <ToastProvider> + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + <Toast key={id} {...props}> + <div className="grid gap-1"> + {title && <ToastTitle>{title}</ToastTitle>} + {description && ( + <ToastDescription>{description}</ToastDescription> + )} + </div> + {action} + <ToastClose /> + </Toast> + ) + })} + <ToastViewport /> + </ToastProvider> + ) +} \ No newline at end of file diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts index c70c0d61949e822e1c5f2530d91aad64ded73d47..2c94c2d4a0c30284eee14f47c9336d0288c4d5fc 100644 --- a/components/ui/use-toast.ts +++ b/components/ui/use-toast.ts @@ -1,7 +1,7 @@ // Inspired by react-hot-toast library import * as React from "react" -import { ToastActionElement, type ToastProps } from "@/components/ui/toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/toast" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 @@ -135,7 +135,7 @@ function dispatch(action: Action) { }) } -interface Toast extends Omit<ToasterToast, "id"> {} +type Toast = Omit<ToasterToast, "id"> function toast({ ...props }: Toast) { const id = genId() diff --git a/components/user-auth-form.tsx b/components/user-auth-form.tsx index 9820124ff8714c1d4fcebcf044b999c03321951e..d02458f15db99a99f38b95217b5442852a195037 100644 --- a/components/user-auth-form.tsx +++ b/components/user-auth-form.tsx @@ -2,7 +2,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { signIn } from 'next-auth/react' -import { useSearchParams } from 'next/navigation' import { HTMLAttributes, useState } from 'react' import { useForm } from 'react-hook-form' import * as z from "zod" @@ -11,10 +10,10 @@ import { Icons } from '@/components/icons' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { useToast } from '@/components/ui/use-toast' +import { ToastAction } from "@/components/ui/toast" +import { toast } from "@/components/ui/use-toast" import { cn } from '@/lib/utils' -import { userAuthSchema } from '@/lib/validations/auth' -import { ToastAction } from "./ui/toast" +import { userAuthSchema } from "@/lib/validations/auth" interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> { type: "login" | "signup" @@ -32,9 +31,6 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { }) const [isLoading, setIsLoading] = useState<boolean>(false) const [isGitHubLoading, setIsGitHubLoading] = useState<boolean>(false) - const searchParams = useSearchParams() - - const { toast } = useToast() async function onSubmit(data: FormData) { setIsLoading(true) @@ -61,19 +57,17 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { }) } } - const signInResult = await signIn("credentials", { - username: data.username, - email: data.email, + usernameOrEmail: data.email?.toLowerCase() || data.usernameOrEmail?.toLowerCase(), password: data.password, - redirect: false, + redirect: true, callbackUrl: "/home", }); setIsLoading(false) - if (!signInResult?.ok) { - toast({ + if (signInResult?.error) { + return toast({ variant: "destructive", title: "Uh oh! Something went wrong.", description: "Your log in request failed. Please try again.", @@ -81,113 +75,142 @@ export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { }) } - // toast({ - // title: "Check your email.", - // description: "We sent you a login link. Be sure to check your spam too.", - // }) + if (type === "signup") { + return toast({ + title: "Congratulations!", + description: "Your account has been created. You will be redirected shortly.", + }) + } else { + return toast({ + title: "Logging in.", + description: "You will be redirected shortly.", + }) + } } return ( - <> - <div className={cn("grid gap-6", className)} {...props}> - <form onSubmit={handleSubmit(onSubmit)}> - <div className="grid gap-2"> + <div className={cn("grid gap-6", className)} {...props}> + <form onSubmit={handleSubmit(onSubmit)}> + <div className="grid gap-2"> + {type === "login" ? <div className="grid gap-1"> - <Label className="sr-only" htmlFor="username"> - Username - </Label> - <Input - id="username" - placeholder="Your username" - type="username" - autoCapitalize="none" - autoComplete="username" - autoCorrect="off" - disabled={isLoading || isGitHubLoading} - {...register("username")} - /> - {errors?.username && ( - <p className="px-1 text-xs text-red-600"> - {errors.username.message} - </p> - )} - </div> - {type === "signup" ? <div className="grid gap-1"> - <Label className="sr-only" htmlFor="email"> - Email + <Label className="sr-only" htmlFor="usernameOrEmail"> + Username or email </Label> <Input - id="email" - placeholder="Your email" - type="email" + id="usernameOrEmail" + placeholder="Your username or email" + type="text" autoCapitalize="none" - autoComplete="email" + autoComplete="username email" autoCorrect="off" disabled={isLoading || isGitHubLoading} - {...register("email")} + {...register("usernameOrEmail", { required: true })} /> - {errors?.email && ( + {errors?.usernameOrEmail && ( <p className="px-1 text-xs text-red-600"> - {errors.email.message} + {errors.usernameOrEmail.message} </p> )} </div> : null} - <div className="grid gap-1"> - <Label className="sr-only" htmlFor="password"> - Password - </Label> - <Input - id="password" - placeholder="Your password" - type="password" - autoCapitalize="none" - autoComplete="new-password" - autoCorrect="off" - disabled={isLoading || isGitHubLoading} - {...register("password")} - /> - {errors?.password && ( - <p className="px-1 text-xs text-red-600"> - {errors.password.message} - </p> - )} - </div> - <Button disabled={isLoading}> - {isLoading && ( - <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> - )} - {type === "signup" ? "Sign Up" : "Log In"} - </Button> + {type === "signup" ? + <> + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="username"> + Username + </Label> + <Input + id="username" + placeholder="Your username" + type="username" + autoCapitalize="none" + autoComplete="username" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("username", { required: true })} + /> + {errors?.username && ( + <p className="px-1 text-xs text-red-600"> + {errors.username.message} + </p> + )} + </div> + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="email"> + Email + </Label> + <Input + id="email" + placeholder="Your email" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("email", { required: true })} + /> + {errors?.email && ( + <p className="px-1 text-xs text-red-600"> + {errors.email.message} + </p> + )} + </div> + </> : null} + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="password"> + Password + </Label> + <Input + id="password" + placeholder="Your password" + type="password" + autoCapitalize="none" + autoComplete="new-password" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("password", { required: true })} + /> + {errors?.password && ( + <p className="px-1 text-xs text-red-600"> + {errors.password.message} + </p> + )} </div> - </form> + <Button disabled={isLoading} type="submit"> + {isLoading && ( + <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> + )} + {type === "signup" ? "Sign Up" : "Log In"} + </Button> + </div> + </form> - <div className="relative"> - <div className="absolute inset-0 flex items-center"> - <span className="w-full border-t" /> - </div> - <div className="relative flex justify-center text-xs uppercase"> - <span className="bg-background px-2 text-muted-foreground"> - Or continue with - </span> - </div> + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground"> + Or continue with + </span> </div> - <Button - variant="outline" - type="button" - onClick={() => { - setIsGitHubLoading(true) - signIn("github") - }} - disabled={isLoading || isGitHubLoading} - > - {isGitHubLoading ? ( - <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> - ) : ( - <Icons.github className="mr-2 h-4 w-4" /> - )}{" "} - Github - </Button> </div> - </> + <Button + variant="outline" + type="button" + onClick={() => { + setIsGitHubLoading(true) + signIn("github") + }} + disabled={isLoading || isGitHubLoading} + > + {isGitHubLoading ? ( + <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> + ) : ( + <Icons.github className="mr-2 h-4 w-4" /> + )}{" "} + Github + </Button> + </div> ) } \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts index 54f645308dfd862b6aaa5ca10292709ac82559d8..92387198ce92ebe162ff7dae993aaed34f555bfb 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -17,42 +17,29 @@ export const authOptions: NextAuthOptions = { }, providers: [ GitHubProvider({ - clientId: env.GITHUB_CLIENT_ID as string, - clientSecret: env.GITHUB_CLIENT_SECRET as string, + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, }), CredentialsProvider({ name: 'Login', credentials: { - username: { label: 'Username', type: 'text' }, - email: { label: 'Email', type: 'email', placeholder: 'hello@example.com' }, + usernameOrEmail: { label: 'Username or Email', type: 'text' }, password: { label: 'Password', type: 'password' } }, async authorize(credentials) { - if (!credentials?.username || !credentials.email || !credentials.password) { + if (!credentials?.usernameOrEmail || !credentials?.password) { return null } - let isUnique = false; - while (!isUnique) { - const existingUserName = await db.user.findUnique({ - where: { - username: credentials.username - } - }) - - if (existingUserName) { - credentials.username = `${credentials.username}${Math.floor(Math.random() * 1000)}` - } else { - isUnique = true; - } - } - - const user = await db.user.findUnique({ + const user = await db.user.findFirst({ where: { - email: credentials.email - } - }) + OR: [ + { username: credentials.usernameOrEmail.toLowerCase() }, + { email: credentials.usernameOrEmail.toLowerCase() }, + ], + }, + }); if (!user) { return null @@ -71,7 +58,6 @@ export const authOptions: NextAuthOptions = { id: user.id, username: user.username, email: user.email, - name: user.name, } } }) diff --git a/lib/validations/auth.ts b/lib/validations/auth.ts index 68de27aa5b125c7ee9c33a78cd4477d52f1a8d15..e749cbd4ef32fae1d8565f774f11a17bde5fae65 100644 --- a/lib/validations/auth.ts +++ b/lib/validations/auth.ts @@ -1,7 +1,13 @@ -import * as z from "zod" +import * as z from "zod"; export const userAuthSchema = z.object({ - username: z.string().min(3).max(15), - email: z.string().email(), - password: z.string().min(6).max(18), -}) \ No newline at end of file + usernameOrEmail: z + .union([ + z.string().min(3, "Username or email must be at least 3 characters").max(15, "Username or email must be at most 15 characters"), + z.string().email("Invalid email format"), + ]) + .optional(), + username: z.string().min(3, "Username must be at least 3 characters").max(15, "Username must be at most 15 characters").optional(), + email: z.string().email("Invalid email format").optional(), + password: z.string().min(6, "Password must be at least 6 characters").max(18, "Password must be at most 18 characters"), +}); \ No newline at end of file