From 798acf992c8654adf47454af4521faecdb5344b5 Mon Sep 17 00:00:00 2001 From: jwoojin9 Date: Mon, 30 Sep 2024 02:35:54 +0900 Subject: [PATCH] refactor(fe): refactor signup input field --- .../components/FeedbackInputField.tsx | 105 ++++ apps/frontend/components/InputMessage.tsx | 30 ++ .../components/auth/SignUpRegister.tsx | 469 ++++++------------ 3 files changed, 289 insertions(+), 315 deletions(-) create mode 100644 apps/frontend/components/FeedbackInputField.tsx create mode 100644 apps/frontend/components/InputMessage.tsx diff --git a/apps/frontend/components/FeedbackInputField.tsx b/apps/frontend/components/FeedbackInputField.tsx new file mode 100644 index 0000000000..09acd48e99 --- /dev/null +++ b/apps/frontend/components/FeedbackInputField.tsx @@ -0,0 +1,105 @@ +import type { Field, SignUpFormInput } from '@/components/auth/SignUpRegister' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' +import { useState } from 'react' +import type { UseFormReturn } from 'react-hook-form' +import { FaEye, FaEyeSlash } from 'react-icons/fa' +import InputMessage from './InputMessage' + +export function FeedbackInputField({ + placeholder, + fieldName, + isError, + enableShowToggle = false, + formMethods, + Messages +}: { + placeholder: string + fieldName: Field + isError: boolean + enableShowToggle?: boolean + formMethods: UseFormReturn + Messages: { + text: string + isVisible: boolean + type?: 'error' | 'description' | 'available' + }[] + children?: React.ReactNode +}) { + const [hasBeenFocused, setHasBeenFocused] = useState(false) + const [isFocused, setIsFocused] = useState(false) + const [passwordShow, setPasswordShow] = useState(true) + const { + register, + getValues, + trigger, + formState: { dirtyFields } + } = formMethods + + return ( +
+
+ trigger(fieldName as keyof SignUpFormInput) + })} + className={cn( + 'focus-visible:ring-0', + (hasBeenFocused || dirtyFields[fieldName]) && + (isError && (getValues(fieldName) || !isFocused) + ? 'border-red-500 focus-visible:border-red-500' + : 'border-primary') + )} + type={passwordShow ? 'text' : 'password'} + onFocus={() => { + trigger(fieldName as keyof SignUpFormInput) + setHasBeenFocused(true) + setIsFocused(true) + }} + onBlur={() => { + setIsFocused(false) + }} + /> + {enableShowToggle && ( + setPasswordShow(!passwordShow)} + > + {passwordShow ? ( + + ) : ( + + )} + + )} +
+ {Messages.filter((message) => { + if (!message.isVisible) return false + switch (message.type) { + case 'error': + return getValues(fieldName) || !isFocused + case 'description': + return !getValues(fieldName) && isFocused + case 'available': + return !isError + } + }).map((message, index) => ( +

+ {InputMessage(message.text, message.type)} +

+ ))} +
+ ) +} diff --git a/apps/frontend/components/InputMessage.tsx b/apps/frontend/components/InputMessage.tsx new file mode 100644 index 0000000000..e5cdebd6ea --- /dev/null +++ b/apps/frontend/components/InputMessage.tsx @@ -0,0 +1,30 @@ +import { cn } from '@/lib/utils' +import { IoWarningOutline } from 'react-icons/io5' + +export default function InputMessage( + message?: string, + type: 'error' | 'description' | 'available' = 'error' +) { + return ( +
+ {message === 'Required' && } +

+ {message} +

+
+ ) +} diff --git a/apps/frontend/components/auth/SignUpRegister.tsx b/apps/frontend/components/auth/SignUpRegister.tsx index 94878d5e0e..5b95c90958 100644 --- a/apps/frontend/components/auth/SignUpRegister.tsx +++ b/apps/frontend/components/auth/SignUpRegister.tsx @@ -1,5 +1,7 @@ 'use client' +import { FeedbackInputField } from '@/components/FeedbackInputField' +import InputMessage from '@/components/InputMessage' import { Button } from '@/components/ui/button' import { Command, @@ -8,7 +10,6 @@ import { CommandInput, CommandItem } from '@/components/ui/command' -import { Input } from '@/components/ui/input' import { Popover, PopoverContent, @@ -25,12 +26,11 @@ import { CommandList } from 'cmdk' import Image from 'next/image' import React, { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' -import { FaCheck, FaChevronDown, FaEye, FaEyeSlash } from 'react-icons/fa' -import { IoWarningOutline } from 'react-icons/io5' +import { FaCheck, FaChevronDown } from 'react-icons/fa' import { toast } from 'sonner' import { z } from 'zod' -interface SignUpFormInput { +export interface SignUpFormInput { username: string email: string verificationCode: string @@ -42,7 +42,7 @@ interface SignUpFormInput { passwordAgain: string } -const FIELD_NAMES = [ +export const FIELD_NAMES = [ 'username', 'password', 'passwordAgain', @@ -51,7 +51,7 @@ const FIELD_NAMES = [ 'studentId', 'major' ] as const -type Field = (typeof FIELD_NAMES)[number] +export type Field = (typeof FIELD_NAMES)[number] const fields: Field[] = [...FIELD_NAMES] const schema = z @@ -92,55 +92,28 @@ const schema = z } ) -export function requiredMessage(message?: string) { - return ( -
- {message === 'Required' && } -

{message}

-
- ) -} - export default function SignUpRegister() { const formData = useSignUpModalStore((state) => state.formData) - const [passwordShow, setPasswordShow] = useState(false) - const [passwordAgainShow, setPasswordAgainShow] = useState(false) - const [inputFocus, setInputFocus] = useState(0) - const [focusedList, setFocusedList] = useState>([ - true, - ...Array(7).fill(false) - ]) + const [majorHasBeenFocused, setMajorHasBeenFocused] = useState(false) const [isUsernameAvailable, setIsUsernameAvailable] = useState(false) const [checkedUsername, setCheckedUsername] = useState('') const [signUpDisable, setSignUpDisable] = useState(false) const [majorOpen, setMajorOpen] = React.useState(false) const [majorValue, setMajorValue] = React.useState('') - const updateFocus = (n: number) => { - setInputFocus(n) - setFocusedList((prevList) => - prevList.map((focused, index) => (index === n ? true : focused)) - ) - if (n > 0) { - trigger(fields[n - 1]) - } - } + const formMethods = useForm({ + resolver: zodResolver(schema), + shouldFocusError: false + }) const { handleSubmit, - register, + setValue, getValues, watch, trigger, formState: { errors, isDirty } - } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - username: '', - password: '' - }, - shouldFocusError: false - }) + } = formMethods const watchedPassword = watch('password') const watchedPasswordAgain = watch('passwordAgain') @@ -152,11 +125,11 @@ export default function SignUpRegister() { }, [watchedPassword, watchedPasswordAgain, trigger]) function onSubmitClick() { - setInputFocus(0) - setFocusedList(Array(8).fill(true)) fields.map((field) => { + setValue(field, getValues(field), { shouldDirty: true }) trigger(field) }) + setMajorHasBeenFocused(true) } const onSubmit = async (data: { @@ -198,15 +171,11 @@ export default function SignUpRegister() { toast.success('Sign up succeeded!') } }) - } catch (error) { + } catch { toast.error('Sign up failed!') setSignUpDisable(false) } } - const validation = async (field: string) => { - await trigger(field as keyof SignUpFormInput) - } - const checkUserName = async () => { const username = getValues('username') await trigger('username') @@ -226,7 +195,6 @@ export default function SignUpRegister() { console.log(err) } } - updateFocus(0) } const isRequiredError = @@ -248,276 +216,147 @@ export default function SignUpRegister() { className="flex w-full flex-col gap-4" onSubmit={handleSubmit(onSubmit)} > -
-
- { - validation('username') - setIsUsernameAvailable(false) - }, - validate: (value) => - value === checkedUsername && isUsernameAvailable - ? true - : 'Check user ID' - })} - onFocus={() => { - trigger('username') - updateFocus(1) - }} - /> - -
-
- {inputFocus !== 1 && ( - <> - {isRequiredError && requiredMessage('Required')} - {isInvalidFormatError && ( -
    -
  • User ID used for log in
  • -
  • 3-10 characters of small letters, numbers
  • -
- )} - {isAvailable && ( -

Available

- )} - {isUnavailable && ( -

This ID is already in use

- )} - {shouldCheckUserId && ( -

Check user ID

- )} - + ]} + formMethods={formMethods} + /> +
-
- -
-
- validation('password') - })} - type={passwordShow ? 'text' : 'password'} - onFocus={() => { - updateFocus(2) - }} - /> - setPasswordShow(!passwordShow)} - > - {passwordShow ? ( - - ) : ( - - )} - -
- {inputFocus === 2 && - (errors.password || !getValues('password') ? ( -
-
    -
  • 8-20 characters
  • -
  • Include two of the followings:
  • -
  • capital letters, small letters, numbers
  • -
-
- ) : ( -

Available

- ))} - {inputFocus !== 2 && - errors.password && - (errors.password.message == 'Required' ? ( - requiredMessage('Required') - ) : ( -
    -
  • 8-20 characters
  • -
  • Include two of the followings:
  • -
  • capital letters, small letters, numbers
  • -
- ))} -
- -
-
- validation('passwordAgain') - })} - className={cn( - 'focus-visible:ring-0', - !focusedList[3] - ? '' - : errors.passwordAgain && - (getValues('passwordAgain') || inputFocus !== 3) - ? 'border-red-500 focus-visible:border-red-500' - : 'border-primary' - )} - placeholder="Re-enter password" - type={passwordAgainShow ? 'text' : 'password'} - onFocus={() => { - updateFocus(3) - }} - /> - setPasswordAgainShow(!passwordAgainShow)} - > - {passwordAgainShow ? ( - - ) : ( - - )} - -
- {errors.passwordAgain && - (getValues('passwordAgain') || inputFocus !== 3) && - requiredMessage(errors.passwordAgain.message)} + disabled={ + (isUsernameAvailable && + checkedUsername == getValues('username')) || + errors.username + ? true + : false + } + size="icon" + > + check +
+ +
-
- validation('firstName') - })} - className={cn( - 'focus-visible:ring-0', - !focusedList[4] - ? '' - : errors.firstName && - (getValues('firstName') || inputFocus !== 4) - ? 'border-red-500 focus-visible:border-red-500' - : 'border-primary' - )} - onFocus={() => { - updateFocus(4) - }} - /> - {errors.firstName && - (getValues('firstName') || inputFocus !== 4) && - requiredMessage(errors.firstName.message)} -
-
- validation('lastName') - })} - className={cn( - 'focus-visible:ring-0', - !focusedList[5] - ? '' - : errors.lastName && - (getValues('lastName') || inputFocus !== 5) - ? 'border-red-500 focus-visible:border-red-500' - : 'border-primary' - )} - onFocus={() => { - updateFocus(5) - }} - /> - {errors.lastName && - (getValues('lastName') || inputFocus !== 5) && - requiredMessage(errors.lastName.message)} -
-
-
- validation('studentId') - })} - className={cn( - 'focus-visible:ring-0', - !focusedList[6] - ? '' - : errors.studentId && - (getValues('studentId') || inputFocus !== 6) - ? 'border-red-500 focus-visible:border-red-500' - : 'border-primary' - )} - onFocus={() => { - updateFocus(6) - }} + + - {errors.studentId && - (getValues('studentId') || inputFocus !== 6) && - requiredMessage(errors.studentId.message)}
+
@@ -526,7 +365,7 @@ export default function SignUpRegister() { variant="outline" role="combobox" onClick={() => { - updateFocus(7) + setMajorHasBeenFocused(true) }} className={cn( 'justify-between border-gray-200 font-normal text-black', @@ -534,7 +373,7 @@ export default function SignUpRegister() { ? 'text-gray-500' : 'ring-primary border-0 ring-1', !majorValue && - focusedList[7] && + majorHasBeenFocused && !majorOpen && 'border-0 ring-1 ring-red-500' )} @@ -579,9 +418,9 @@ export default function SignUpRegister() { {!majorValue && - focusedList[7] && + majorHasBeenFocused && !majorOpen && - requiredMessage('Required')} + InputMessage('Required')}