Skip to content

Commit

Permalink
refactor: optimize sign in form with react-hook-form
Browse files Browse the repository at this point in the history
  • Loading branch information
jsun969 committed Jan 13, 2024
1 parent fafc025 commit 4acca59
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 58 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
"dependencies": {
"@clerk/clerk-react": "^4.30.3",
"@clerk/nextjs": "^4.29.3",
"@hookform/resolvers": "^3.3.4",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"react-icons": "^4.12.0",
"zod": "^3.22.4"
},
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 41 additions & 54 deletions src/app/(account)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,48 @@
'use client';

import Button from '@/components/Button';
import ControlledField from '@/components/ControlledField';
import FancyRectangle from '@/components/FancyRectangle';
import Field from '@/components/Field';
import validateFields from '@/util/validation';
import { useSignIn } from '@clerk/clerk-react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';

// Define validation schemas
const emailSchema = z
.string()
.min(1, { message: 'Please enter your email' })
.email({ message: 'Please enter a valid email' });
const passwordSchema = z.string().min(1, { message: 'Please enter your password' });
const signInSchema = z.object({
email: z
.string()
.min(1, { message: 'Please enter your email' })
.email({ message: 'Please enter a valid email' }),
password: z.string().min(1, { message: 'Please enter your password' }),
});

const CLERK_SIGN_IN_ERRORS = {
form_identifier_not_found: { field: 'email', message: "Can't find your account" },
form_password_incorrect: {
field: 'password',
message: 'Password is incorrect. Try again, or use another method.',
},
strategy_for_user_invalid: {
field: 'password',
message: 'Account is not set up for password sign-in. Please sign in with Google.',
},
} as const;

export default function SignInForm() {
const { isLoaded, signIn, setActive } = useSignIn();
const [emailAddress, setEmailAddress] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);

const handleSignIn = async (e: React.ChangeEvent<any>) => {
e.preventDefault();

const isValid = validateFields(
[emailAddress, password],
[emailSchema, passwordSchema],
[setEmailError, setPasswordError]
);

if (!isValid) return;
const form = useForm<z.infer<typeof signInSchema>>({
defaultValues: { email: '', password: '' },
resolver: zodResolver(signInSchema),
});

const handleSignIn = form.handleSubmit(async (formData) => {
if (!isLoaded) return;

try {
const result = await signIn.create({
identifier: emailAddress,
password,
identifier: formData.email,
password: formData.password,
});

if (result.status === 'complete') {
Expand All @@ -48,20 +51,15 @@ export default function SignInForm() {
console.log(result);
}
} catch (err: any) {
// Handle any errors that might occur during the sign-in process
if (err.errors[0].code === 'form_identifier_not_found') {
setEmailError("Can't find your account");
} else if (err.errors[0].code === 'form_password_incorrect') {
setPasswordError('Password is incorrect. Try again, or use another method.');
} else if (err.errors[0].code === 'strategy_for_user_invalid') {
setPasswordError(
'Account is not set up for password sign-in. Please sign in with Google.'
);
const errorCode = err.errors[0].code as string;
if (errorCode in CLERK_SIGN_IN_ERRORS) {
const error = CLERK_SIGN_IN_ERRORS[errorCode as keyof typeof CLERK_SIGN_IN_ERRORS];
form.setError(error.field, { message: error.message });
} else {
console.error(err);
}
}
};
});

const handleGoogleSignIn = async () => {
try {
Expand Down Expand Up @@ -99,19 +97,13 @@ export default function SignInForm() {
<p className="mx-4 text-grey">or</p>
<div className="border-t border-grey w-full"></div>
</div>
<form>
<Field
label="Email"
value={emailAddress}
onChange={(value) => setEmailAddress(value)}
error={emailError}
/>
<Field
<form onSubmit={handleSignIn}>
<ControlledField label="Email" control={form.control} name="email" />
<ControlledField
label="Password"
value={password}
onChange={(value) => setPassword(value)}
control={form.control}
name="password"
type="password"
error={passwordError}
/>
{/* Forgot passwort */}
{/* TODO: Implement forgot password */}
Expand All @@ -121,20 +113,15 @@ export default function SignInForm() {
>
Forgot password?
</a>
<Button
onClick={handleSignIn}
type="submit"
colour="orange"
width="w-[19rem] md:w-[25.5rem]"
>
<Button type="submit" colour="orange" width="w-[19rem] md:w-[25.5rem]">
Sign In
</Button>
</form>

{/* Sign-up option */}
<div className="flex mt-10">
<p className="text-grey text-lg md:text-base">
Don't have an account yet?{' '}
Don&apos;t have an account yet?{' '}
<a href="/join-us" className="text-orange">
Join Us
</a>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface ButtonProps {
width?: string;
}

const Button = ({ children, colour, href, onClick, width }: ButtonProps) => {
const Button = ({ children, colour, href, onClick, width, type }: ButtonProps) => {
const isAnchor = !!href;
const Component = isAnchor ? 'a' : 'button';

Expand All @@ -27,7 +27,7 @@ const Button = ({ children, colour, href, onClick, width }: ButtonProps) => {
e: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLAnchorElement>
) => void | Promise<void>
}
type={isAnchor ? undefined : 'button'}
type={isAnchor ? undefined : type}
className={buttonClasses}
>
{children}
Expand Down
30 changes: 30 additions & 0 deletions src/components/ControlledField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Control, FieldPath, FieldValues, useController } from 'react-hook-form';
import Field, { FieldProps } from './Field';

interface ControlledFieldProps<TFieldValues extends FieldValues>
extends Omit<FieldProps, 'value' | 'onChange' | 'error'> {
control: Control<TFieldValues>;
name: FieldPath<TFieldValues>;
}

/**
* `Field` controlled by react hook form
*/
const ControlledField = <TFieldValues extends FieldValues = FieldValues>({
control,
name,
...props
}: ControlledFieldProps<TFieldValues>) => {
const { field, fieldState } = useController({ control, name });

return (
<Field
value={field.value}
onChange={field.onChange}
error={fieldState.error?.message}
{...props}
/>
);
};

export default ControlledField;
4 changes: 2 additions & 2 deletions src/components/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ChangeEvent, useState } from 'react';
import { ChangeEvent, useState } from 'react';
import { IoEye, IoEyeOff } from 'react-icons/io5';

interface FieldProps {
export interface FieldProps {
label: string;
value: string;
onChange: (value: string) => void;
Expand Down

0 comments on commit 4acca59

Please sign in to comment.