|
1 | 1 | 'use client';
|
2 | 2 |
|
| 3 | +import { zodResolver } from '@hookform/resolvers/zod'; |
3 | 4 | import { Button } from '@nextui-org/button';
|
4 | 5 | import { Input } from '@nextui-org/input';
|
5 | 6 | import { Spinner } from '@nextui-org/spinner';
|
6 | 7 | import { LogIn, UserPlus, Mail, Lock } from 'lucide-react';
|
7 |
| -import { useTransition, useRef } from 'react'; |
| 8 | +import { useRef, useState, useTransition } from 'react'; |
| 9 | +import { useForm, Controller } from 'react-hook-form'; |
8 | 10 | import { toast } from 'sonner';
|
| 11 | +import { z } from 'zod'; |
9 | 12 |
|
10 | 13 | import { Logo } from '@/components/icons';
|
11 |
| -import { GitHubIcon } from '@/components/icons/GithubIcon'; |
12 |
| -import { GoogleIcon } from '@/components/icons/GoogleIcon'; |
13 | 14 | import { formVariants } from '@/components/primitives';
|
14 |
| -import { DividerText } from '@/components/ui/DividerText'; |
15 | 15 | import { Form } from '@/components/ui/form/Form';
|
16 | 16 | import { InputPassword } from '@/components/ui/form/InputPassword';
|
17 |
| -import { login, signup, type AuthActionFn } from '@/features/auth/actions/baseAuth'; |
18 |
| -import { githubLogin, googleLogin } from '@/features/auth/actions/baseOAuth'; |
| 17 | +import { EmailLoginSchema } from '@/features/auth/actions/validators/emailLoginSchema'; |
| 18 | +import { AuthTabs, type AuthTabKey } from '@/features/auth/components/AuthTabs'; |
| 19 | +import { upperFirst } from '@/lib/utils/upperFirst'; |
| 20 | +import { humanizeError } from '@/lib/zod/humanizeError'; |
19 | 21 |
|
20 |
| -interface IProps { |
| 22 | +export interface IAuthLoginFormProps { |
21 | 23 | message?: string;
|
| 24 | + onSubmit?: (action: AuthTabKey, credentials: z.infer<typeof EmailLoginSchema>) => Promise<void>; |
| 25 | + children?: React.ReactNode; |
22 | 26 | }
|
23 | 27 |
|
24 |
| -export const AuthLoginForm: RC<IProps> = ({ message }) => { |
| 28 | +export const AuthLoginForm: RC<IAuthLoginFormProps> = ({ message, onSubmit, children }) => { |
| 29 | + const [activeTab, setActiveTab] = useState<AuthTabKey>('login'); |
| 30 | + |
| 31 | + const { control, ...form } = useForm({ |
| 32 | + resolver: zodResolver(EmailLoginSchema), |
| 33 | + mode: 'all', |
| 34 | + defaultValues: { |
| 35 | + email: '', |
| 36 | + password: '', |
| 37 | + }, |
| 38 | + }); |
| 39 | + |
25 | 40 | const formRef = useRef<HTMLFormElement | null>(null);
|
26 |
| - const [isSubmitting, startTransition] = useTransition(); |
27 | 41 |
|
28 | 42 | const icoProps = {
|
29 | 43 | className: 'opacity-45',
|
30 | 44 | size: 19,
|
31 | 45 | };
|
32 | 46 |
|
33 |
| - // With transition wrapper allows tracking the transition pending state |
34 |
| - const withTransition = (fn: AuthActionFn) => () => { |
35 |
| - startTransition(async () => { |
36 |
| - try { |
37 |
| - const [, err] = (await fn(new FormData(formRef.current!))) ?? []; // next.js redirects from sa -> returns undefined, so we use empty array as fallback |
38 |
| - |
39 |
| - if (err) { |
40 |
| - throw new Error(err.message); // coz: fn is safe async function |
41 |
| - } |
42 |
| - toast.success('Login successful'); |
43 |
| - } catch (e) { |
44 |
| - toast.error(e.message); |
45 |
| - } |
46 |
| - }); |
47 |
| - }; |
| 47 | + const [isSubmitting, startTransition] = useTransition(); |
| 48 | + |
| 49 | + const handleSubmit = form.handleSubmit( |
| 50 | + async (credentials) => { |
| 51 | + startTransition(async () => { |
| 52 | + await onSubmit?.(activeTab, credentials); |
| 53 | + }); |
| 54 | + }, |
| 55 | + (errors) => { |
| 56 | + toast.error(humanizeError(errors)); |
| 57 | + }, |
| 58 | + ); |
48 | 59 |
|
49 | 60 | return (
|
50 | 61 | <div className="container relative flex w-full flex-col justify-center gap-4 px-8 sm:max-w-sm">
|
51 | 62 | <h1 className="text-center text-4xl">
|
52 | 63 | <Logo className="m-auto size-20" />
|
53 | 64 | <span className="sr-only">Login</span>
|
54 | 65 | </h1>
|
| 66 | + |
55 | 67 | {isSubmitting && <Spinner className="absolute inset-0 m-auto" size="lg" title="Logging in..." />}
|
56 |
| - <Form ref={formRef} className={formVariants({ isSubmitting })} size="lg" variant="bordered"> |
57 |
| - <Input name="email" placeholder="you@example.com" startContent={<Mail {...icoProps} />} /> |
58 |
| - <InputPassword name="password" placeholder="••••••••" startContent={<Lock {...icoProps} />} /> |
59 |
| - <Button color="primary" variant="ghost" onClick={withTransition(login)}> |
60 |
| - <LogIn /> Sign In |
61 |
| - </Button> |
62 |
| - <Button onClick={withTransition(signup)}> |
63 |
| - <UserPlus /> Sign Up |
64 |
| - </Button> |
65 |
| - <DividerText text="or" /> |
66 |
| - <Button onClick={withTransition(googleLogin)}> |
67 |
| - <GoogleIcon /> Google |
68 |
| - </Button> |
69 |
| - <Button onClick={withTransition(githubLogin)}> |
70 |
| - <GitHubIcon /> GitHub |
| 68 | + |
| 69 | + <AuthTabs active={activeTab} onTabChange={setActiveTab} /> |
| 70 | + |
| 71 | + <Form ref={formRef} className={formVariants({})} size="lg" variant="bordered" onSubmit={handleSubmit}> |
| 72 | + <Controller |
| 73 | + control={control} |
| 74 | + name="email" |
| 75 | + render={({ field }) => ( |
| 76 | + <Input {...field} name="email" placeholder="you@example.com" startContent={<Mail {...icoProps} />} /> |
| 77 | + )} |
| 78 | + /> |
| 79 | + <Controller |
| 80 | + control={control} |
| 81 | + name="password" |
| 82 | + render={({ field }) => <InputPassword {...field} startContent={<Lock {...icoProps} />} />} |
| 83 | + /> |
| 84 | + |
| 85 | + <Button color="primary" type="submit" variant="ghost"> |
| 86 | + {activeTab === 'login' ? <LogIn /> : <UserPlus />} |
| 87 | + {upperFirst(activeTab)} |
71 | 88 | </Button>
|
| 89 | + |
| 90 | + {children} |
72 | 91 | </Form>
|
73 | 92 |
|
74 | 93 | {message && <p className="mt-4 bg-foreground/10 p-4 text-center text-foreground">{message}</p>}
|
|
0 commit comments