From b4ca3032ceeeaf253c8e541033acb8f7b192ea86 Mon Sep 17 00:00:00 2001 From: jwoojin9 <44637040+jwoojin9@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:44:11 +0900 Subject: [PATCH] feat(fe): redesign signup modal (#1911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(fe): redesign signup modal * fix(be): fix bruno query grammar (#1860) * chore(be): return including enable-copy-paste (#1867) * fix(deps): update dependency @codemirror/view to ^6.28.6 (#1838) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency vitest to v2 (#1839) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency @sentry/nextjs to v8 (#1731) * fix(deps): update dependency @sentry/nextjs to v8 * fix(fe): update withSentryconfig usage --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jimin Ha * fix(deps): update dependency nestjs-otel to v6 (#1732) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update all non-major dependencies (#1808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(ci): disable backend e2e-test on cd-stage (#1871) * chore: update issue template (#1876) Notion task와의 연동을 위한 사전 작업입니다. * fix: fix typo (#1877) * fix(fe): fix sentry initialization (#1873) * fix(fe): fix sentry initialization * refactor(fe): simplify sentry init logic * fix(infra): separate local minio and stage minio container (#1887) * fix(infra): separate local minio and stage minio container * fix(infra): change devcontainer settings of local-minio * fix(infra): 1886 separate stage local minio container (#1888) * fix(infra): separate local minio and stage minio container * fix(infra): change devcontainer settings of local-minio * fix(infra): change setup container depends-on container * chore(infra): fix typo * feat(fe): add editor copy paste preventing (#1874) * feat(fe): add editor copy paste preventing * fix(fe): resolve error in submission by adding initial value to enableCopyPaste * fix(fe): resolve error in non-contest problem by adding initial value * fix(fe): prevent copy paste when enableCopyPaste is false * chore(fe): improve code readability * feat(fe): implement image upload (#1862) * feat(fe): implement image upload * fix(fe): fix pnpm lock file * feat(fe): refactor admin contest structure (#1863) * chore: refactor admin contest folder structure * refactor(fe): refactor admin create contest * refactor(fe): refactor edit contest page * chore: refactor zod schema, lifting inputStyle * feat(fe): add show state for hint and source for initial state of switch * refactor: componentize contest problem list label and button * feat(fe): add contest detail in contest overall page * chore: move directory of components only used in contest * feat(fe): implement contest overall tabs using parallel routes * chore: wrap tabs with overall folder * chore(fe): edit color of tailwind config (#1893) * chore: edit tailwind color config * chore: apply level color at badge * chore: change text and bg to dark and light * feat(fe): redesign main page carousel (#1900) * feat(fe): update carousel * feat(fe): replace mono font * chore(fe): fix carousel margin * chore(fe): change font weight * fix(fe): fix css * feat(be): add student-id and major field (#1806) * feat(be): add student-id and major field * feat(be): prisma migrate for add major, student-id * feat(be): change default value to dto value * feat(be): change seed.ts file * feat(be): change student-id and major optional * feat(be): prisma migrate * feat(be): error fix * feat(be): type error fix * feat(be): delete annotation * feat(be): restore bruno * feat(be): review edit * feat(be): add default value * chore(be): rename student-id * docs(be): add student-id and major * docs(be): student-id fix --------- Co-authored-by: Dayong Lee Co-authored-by: Jaehyeon Kim Co-authored-by: Jaehyeon Kim <65964601+Jaehyeon1020@users.noreply.github.com> * chore(be): add cascade on announcement model (#1905) * fix(be): remove unique constraint on student-id field (#1906) * fix(be): remove unique constraint on student-id field * chore(be): add migration * fix(fe): add controlller to major combobox * feat(fe): redesign signup modal * feat(fe): redesign signup information fields * fix(fe): cancel package.json change * feat(fe): redesign signup modal and fix email timer * chore(fe): improve code readability * chore(fe): improve code readability * chore(fe): improve code redability * fix(fe): change code for readability * chore(fe): erase comment --------- Co-authored-by: jimin9038 Co-authored-by: Jaehyeon Kim Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Taehoon Kim <95288696+goathoon@users.noreply.github.com> Co-authored-by: YooJin Lee <113789141+youznn@users.noreply.github.com> Co-authored-by: Kwon Seo Jin <97675977+B0XERCAT@users.noreply.github.com> Co-authored-by: hwantae <73492261+hwantae@users.noreply.github.com> Co-authored-by: Dayong Lee Co-authored-by: Jaehyeon Kim <65964601+Jaehyeon1020@users.noreply.github.com> Co-authored-by: Jiho Park <59248080+jihorobert@users.noreply.github.com> Co-authored-by: jiho --- apps/frontend/components/auth/SignUp.tsx | 30 +- .../components/auth/SignUpEmailVerify.tsx | 186 +++--- .../components/auth/SignUpRegister.tsx | 547 ++++++++++++++---- .../components/auth/SignUpWelcome.tsx | 8 +- 4 files changed, 583 insertions(+), 188 deletions(-) diff --git a/apps/frontend/components/auth/SignUp.tsx b/apps/frontend/components/auth/SignUp.tsx index dce82e008a..735a71f5f8 100644 --- a/apps/frontend/components/auth/SignUp.tsx +++ b/apps/frontend/components/auth/SignUp.tsx @@ -26,28 +26,30 @@ export default function SignUp() { )} codedang {modalPage === 0 && } {modalPage === 1 && } {modalPage === 2 && } -
- - Already have account? - - -
+ {modalPage === 0 && ( +
+ + Already have account? + + +
+ )} ) } diff --git a/apps/frontend/components/auth/SignUpEmailVerify.tsx b/apps/frontend/components/auth/SignUpEmailVerify.tsx index dab4fe0bf2..f19c714bd0 100644 --- a/apps/frontend/components/auth/SignUpEmailVerify.tsx +++ b/apps/frontend/components/auth/SignUpEmailVerify.tsx @@ -4,9 +4,8 @@ import { baseUrl } from '@/lib/constants' import { cn } from '@/lib/utils' import useSignUpModalStore from '@/stores/signUpModal' import { zodResolver } from '@hookform/resolvers/zod' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { useForm } from 'react-hook-form' -import { useInterval } from 'react-use' import { z } from 'zod' interface EmailVerifyInput { @@ -26,6 +25,8 @@ const timeLimit = 300 export default function SignUpEmailVerify() { const [timer, setTimer] = useState(timeLimit) + const intervalRef = useRef(null) + const previousTimeRef = useRef(Date.now()) const [expired, setExpired] = useState(false) const { nextModal, setFormData } = useSignUpModalStore((state) => state) const { @@ -43,21 +44,34 @@ export default function SignUpEmailVerify() { const [codeError, setCodeError] = useState('') const [emailVerified, setEmailVerified] = useState(false) const [emailAuthToken, setEmailAuthToken] = useState('') - - useInterval( - () => { - if (timer > 0) { - setTimer((prevTimer) => prevTimer - 1) - } - }, - sentEmail ? 1000 : null - ) + const [sendButtonDisabled, setSendButtonDisabled] = useState(false) useEffect(() => { - if (timer === 0) { - setExpired(true) + if (sentEmail && !expired) { + previousTimeRef.current = Date.now() + intervalRef.current = setInterval(() => { + const currentTime = Date.now() + const elapsedTime = (currentTime - previousTimeRef.current) / 1000 + previousTimeRef.current = currentTime + + setTimer((prevTimer) => { + const updatedTimer = Math.max(prevTimer - Math.round(elapsedTime), 0) + if (updatedTimer === 0) { + setExpired(true) + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + } + return updatedTimer + }) + }, 1000) + } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + } } - }, [timer]) + }, [sentEmail, expired]) const formatTimer = () => { const minutes = Math.floor(timer / 60) @@ -77,8 +91,9 @@ export default function SignUpEmailVerify() { const sendEmail = async () => { const { email } = getValues() setEmailContent(email) + setEmailError('') await trigger('email') - if (!errors.email && !sentEmail) { + if (!errors.email) { await fetch(baseUrl + '/email-auth/send-email/register-new', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -86,10 +101,12 @@ export default function SignUpEmailVerify() { }) .then((res) => { if (res.status === 409) { - setEmailError('You have already signed up!') + setEmailError('You have already signed up') + setSendButtonDisabled(false) } else if (res.status === 201) { setSentEmail(true) setEmailError('') + setSendButtonDisabled(false) } }) .catch(() => { @@ -116,91 +133,124 @@ export default function SignUpEmailVerify() { setCodeError('') setEmailAuthToken(response.headers.get('email-auth') || '') } else { - setCodeError('Verification code is not valid!') + setCodeError('Verification code is not valid') + setEmailVerified(false) } } catch { - setCodeError('Email verification failed!') + setEmailVerified(false) + setCodeError('Email verification failed') } } } return ( -
- {sentEmail && !expired && ( -

{formatTimer()}

- )} -

Sign Up

+ +

+ Sign up +

{!sentEmail && ( - { - if (e.key === 'Enter') { - e.preventDefault() - sendEmail() - } - }} - /> - )} - {errors.email && ( -

{errors.email?.message}

+
+ { + if (e.key === 'Enter' && !sendButtonDisabled) { + e.preventDefault() + setSendButtonDisabled(true) + sendEmail() + } + }} + /> + {errors.email && ( +

{errors.email?.message}

+ )} + {emailError && ( +

{emailError}

+ )} +
)} -

{emailError}

{sentEmail && ( - <> -
- {emailContent} +
+
+
{emailContent}
+ {sentEmail && !expired && ( +

{formatTimer()}

+ )}
verifyCode() })} /> - - )} -

- {errors.verificationCode ? errors.verificationCode?.message : codeError} -

- {sentEmail && - !expired && - !errors.verificationCode && - codeError === '' && - !emailVerified && ( -

We've sent an email!

- )} - {expired && ( -

- Verification code expired -
- Please resend an email and try again -

+ {sentEmail && + !expired && + !errors.verificationCode && + !codeError && + !emailVerified && ( +

+ We've sent an email +

+ )} + {expired && ( +

+ Verification code expired +
+ Please resend an email and try again +

+ )} + {!expired && ( +

+ {errors.verificationCode + ? errors.verificationCode?.message + : codeError} +

+ )} +
)} + {!sentEmail ? ( ) : !expired ? ( ) : (
- {inputFocus === 2 && ( -
-
    -
  • User ID used for log in
  • -
  • - Your ID must be 3-10 characters of small -
    - alphabet letters, numbers -
  • -
-
- )} - {errors.username ? ( -

Unavailable

- ) : ( - usernameVerify && - (disableUsername ? ( -

Available

- ) : ( -

Unavailable

- )) - )} +
+ {inputFocus !== 1 && ( + <> + {isRequiredError && requiredMessage('Required')} + {isInvalidFormatError && ( +
    +
  • User ID used for log in
  • +
  • 3-10 characters of small letters, numbers
  • +
+ )} + {isAvailable && ( +

Available

+ )} + {isUnavailable &&

Unavailable

} + {shouldCheckUserId && ( +

Check user ID

+ )} + + )} + {inputFocus === 1 && + (!isUsernameAvailable && + checkedUsername === getValues('username') && + getValues('username') ? ( +

Unavailable

+ ) : ( +
+
    +
  • User ID used for log in
  • +
  • 3-10 characters of small letters, numbers
  • +
+
+ ))} +
-
+
validation('password') })} type={passwordShow ? 'text' : 'password'} onFocus={() => { - setInputFocus(3) + updateFocus(2) }} /> setPasswordShow(!passwordShow)} > {passwordShow ? ( @@ -225,38 +426,56 @@ export default function SignUpRegister() { )}
- {inputFocus === 3 && ( -
-
    -
  • - Your password must be at least 8 characters -
  • -
  • and include two of the followings:
  • -
  • Capital letters, Small letters, or Numbers
  • + {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( + focusedList[3] && 'ring-1 focus-visible:ring-1', + errors.passwordAgain && + (getValues('passwordAgain') || inputFocus !== 3) + ? 'ring-red-500 focus-visible:ring-red-500' + : 'ring-primary' + )} placeholder="Re-enter password" type={passwordAgainShow ? 'text' : 'password'} onFocus={() => { - setInputFocus(4) + updateFocus(3) }} /> setPasswordAgainShow(!passwordAgainShow)} > {passwordAgainShow ? ( @@ -266,15 +485,139 @@ export default function SignUpRegister() { )}
- {errors.passwordAgain && ( -

Incorrect

- )} + {errors.passwordAgain && + (getValues('passwordAgain') || inputFocus !== 3) && + requiredMessage(errors.passwordAgain.message)} +
+
+
+
+ validation('firstName') + })} + className={cn( + focusedList[4] && 'ring-1 focus-visible:ring-1', + errors.firstName && (getValues('firstName') || inputFocus !== 4) + ? 'ring-red-500 focus-visible:ring-red-500' + : 'ring-primary' + )} + onFocus={() => { + updateFocus(4) + }} + /> + {errors.firstName && + (getValues('firstName') || inputFocus !== 4) && + requiredMessage(errors.firstName.message)} +
+
+ validation('lastName') + })} + className={cn( + focusedList[5] && 'ring-1 focus-visible:ring-1', + errors.lastName && (getValues('lastName') || inputFocus !== 5) + ? 'ring-red-500 focus-visible:ring-red-500' + : 'ring-primary' + )} + onFocus={() => { + updateFocus(5) + }} + /> + {errors.lastName && + (getValues('lastName') || inputFocus !== 5) && + requiredMessage(errors.lastName.message)} +
+
+
+ validation('studentId') + })} + className={cn( + focusedList[6] && 'ring-1 focus-visible:ring-1', + errors.studentId && (getValues('studentId') || inputFocus !== 6) + ? 'ring-red-500 focus-visible:ring-red-500' + : 'ring-primary' + )} + onFocus={() => { + updateFocus(6) + }} + /> + {errors.studentId && + (getValues('studentId') || inputFocus !== 6) && + requiredMessage(errors.studentId.message)} +
+
+ + + + + + + + + No major found. + + + {majors?.map((major) => ( + { + setMajorValue(currentValue) + setMajorOpen(false) + }} + > + + {major} + + ))} + + + + + + + {!majorValue && + focusedList[7] && + !majorOpen && + requiredMessage('Required')}
diff --git a/apps/frontend/components/auth/SignUpWelcome.tsx b/apps/frontend/components/auth/SignUpWelcome.tsx index 20a92b83c7..255f13413f 100644 --- a/apps/frontend/components/auth/SignUpWelcome.tsx +++ b/apps/frontend/components/auth/SignUpWelcome.tsx @@ -12,11 +12,11 @@ import useSignUpModalStore from '@/stores/signUpModal' export default function SignUpWelcome() { const { nextModal } = useSignUpModalStore((state) => state) return ( -
-

- "Welcome to CODEDANG" +

+

+ WELCOME to CODEDANG

- {/*