From 6a7ac12848719be6dfe30169880b47e82416539f Mon Sep 17 00:00:00 2001 From: sryung Date: Mon, 25 Dec 2023 02:25:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[=F0=9F=92=8E=20:=20refactor]=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로필 수정 기능 확대를 위해 profile 관련 컴포넌트 분리 작업 진행 --- src/components/edit-profile-form.tsx | 3 ++ src/components/user-profile.tsx | 57 ++++++++++++++++++++++++++ src/routes/profile.tsx | 60 +--------------------------- src/styles/profile.ts | 20 ++++++++-- 4 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 src/components/edit-profile-form.tsx create mode 100644 src/components/user-profile.tsx diff --git a/src/components/edit-profile-form.tsx b/src/components/edit-profile-form.tsx new file mode 100644 index 0000000..d483196 --- /dev/null +++ b/src/components/edit-profile-form.tsx @@ -0,0 +1,3 @@ +export default function EditProfileForm() { + return
; +} diff --git a/src/components/user-profile.tsx b/src/components/user-profile.tsx new file mode 100644 index 0000000..b07bc27 --- /dev/null +++ b/src/components/user-profile.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { doc, getDoc } from 'firebase/firestore'; +import { auth, db } from '../firebase.ts'; +import * as S from '../styles/profile.ts'; +import * as P from '../styles/popup.ts'; +import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; +import EditProfileForm from './edit-profile-form.tsx'; + +export default function UserProfile() { + const user = auth.currentUser; + const [userAvatar, setUserAvatar] = useState(''); + const [userName, setUserName] = useState(''); + useEffect(() => { + const fetchUserData = async () => { + if (!user) return; + const userDoc = await getDoc(doc(db, 'users', user?.uid)); + if (userDoc.exists()) { + const userData = userDoc.data(); + setUserAvatar(userData.userAvatar); + setUserName(userData.userName); + } + }; + fetchUserData(); + }, []); + const [editPopup, setEditPopup] = useState(false); + const toggleEditPopup = () => { + setEditPopup(!editPopup); + }; + return ( + + + {userAvatar ? ( + + ) : ( + + )} + + {userName} + + 프로필 수정 + + {editPopup ? ( + + + + + + + ) : null} + + ); +} diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index ea36f36..21204c6 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -1,69 +1,13 @@ -import React, { useState } from 'react'; -import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; -import { updateProfile } from 'firebase/auth'; -import { doc, updateDoc } from 'firebase/firestore'; -import { auth, db, storage } from '../firebase.ts'; -import CompressImage from '../utils/compress-image.tsx'; import WindowTop from '../components/window-top.tsx'; import UserTimeline from '../components/user-timeline.tsx'; import * as W from '../styles/window.ts'; -import * as S from '../styles/profile.ts'; -import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; +import UserProfile from '../components/user-profile.tsx'; export default function Profile() { - const user = auth.currentUser; - const [avatar, setAvatar] = useState(user?.photoURL); - const updateUserAvatar = async (uid: string, newAvatarUrl: string) => { - const userDocRef = doc(db, 'users', uid); - await updateDoc(userDocRef, { - userAvatar: newAvatarUrl, - }); - }; - const onAvatarChange = async (e: React.ChangeEvent) => { - const images = e.target.files; - if (!user) return; - if (images && images.length === 1) { - const selectedImage = images[0]; - const compressedImage = await CompressImage({ - imageFile: selectedImage, - size: 120, - }); - const locationRef = ref(storage, `avatars/${user?.uid}`); - if (compressedImage) { - const result = await uploadBytes(locationRef, compressedImage); - const avatarUrl = await getDownloadURL(result.ref); - setAvatar(avatarUrl); - await updateProfile(user, { - photoURL: avatarUrl, - }); - await updateUserAvatar(user.uid, avatarUrl); - } - } - }; return ( - - - {avatar ? ( - - ) : ( - - )} - - - {user?.displayName ?? 'Anonymous'} - + ); diff --git a/src/styles/profile.ts b/src/styles/profile.ts index 56580a7..a1afb71 100644 --- a/src/styles/profile.ts +++ b/src/styles/profile.ts @@ -1,7 +1,9 @@ import { styled } from 'styled-components'; +import React from 'react'; import { grayColor } from './global.ts'; +import { LineButton } from './button.ts'; -export const Avatar = styled.div` +export const Profile = styled.article` position: relative; display: flex; flex-direction: column; @@ -21,7 +23,7 @@ export const Avatar = styled.div` } `; -export const AvatarUpload = styled.label` +export const Avatar = styled.div` position: relative; display: flex; justify-content: center; @@ -30,7 +32,6 @@ export const AvatarUpload = styled.label` width: 120px; height: 120px; border-radius: 50%; - cursor: pointer; svg { width: 40px; stroke: ${grayColor}; @@ -49,6 +50,14 @@ export const AvatarUpload = styled.label` } `; +export const AvatarUpload: React.ComponentType< + React.HTMLProps +> = styled(Avatar).attrs(() => ({ + as: 'label', +}))` + cursor: pointer; +`; + export const AvatarImage = styled.img` width: 100%; height: 100%; @@ -63,3 +72,8 @@ export const AvatarInput = styled.input` export const Name = styled.h2` font-size: 30px; `; + +export const EditButton = styled(LineButton)` + width: auto; + margin: 0; +`; From 58e15ee3b708ebb3b03ea5b40cbd52114a4f3880 Mon Sep 17 00:00:00 2001 From: sryung Date: Tue, 26 Dec 2023 22:33:45 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20UI=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#9)=20=EB=B0=8F=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로필 수정 컴포넌트를 팝업으로 분리 (edit-profile-form) 그로인한 스타일링 작업 - 유저 이름 변경 기능 추가. users 문서의 userName 필드 수정을 위해서는 updateDoc 메서드를, user 객체의 displayName 수정을 위해서는 updateProfile 메서드를 활용. - 프로필 이미지 제거 기능 추가. 마찬가지로 user 객체의 photoURL 함께 변경되도록 작업 - 추가 작업 필요) 변경된 유저 정보에 따라 자동 리렌더링 필요 --- src/components/edit-profile-form.tsx | 132 ++++++++++++++++++++++++++- src/components/edit-tweet-form.tsx | 22 +++-- src/components/tweet.tsx | 2 +- src/components/user-profile.tsx | 6 +- src/interfaces/IUser.ts | 5 + src/styles/profile-form.ts | 78 ++++++++++++++++ src/styles/profile.ts | 15 +-- src/styles/tweet-form.ts | 18 +--- 8 files changed, 239 insertions(+), 39 deletions(-) create mode 100644 src/interfaces/IUser.ts create mode 100644 src/styles/profile-form.ts diff --git a/src/components/edit-profile-form.tsx b/src/components/edit-profile-form.tsx index d483196..dd6be9e 100644 --- a/src/components/edit-profile-form.tsx +++ b/src/components/edit-profile-form.tsx @@ -1,3 +1,131 @@ -export default function EditProfileForm() { - return
; +import React, { useState } from 'react'; +import { updateProfile } from 'firebase/auth'; +import { doc, updateDoc } from 'firebase/firestore'; +import { + deleteObject, + getDownloadURL, + ref, + uploadBytes, +} from 'firebase/storage'; +import { auth, db, storage } from '../firebase.ts'; +import IUser from '../interfaces/IUser.ts'; +import CompressImage from '../utils/compress-image.tsx'; +import * as S from '../styles/profile-form.ts'; +import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; +import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; + +interface IEditProfileForm extends Pick { + onClose: () => void; +} + +export default function EditProfileForm({ + userAvatar: initialAvatar, + userName: initialName, + onClose, +}: IEditProfileForm) { + const user = auth.currentUser; + const [isLoading, setLoading] = useState(false); + const [avatar, setAvatar] = useState(null); + const [avatarPreview, setAvatarPreview] = useState(user?.photoURL); + const onAvatarChange = async (e: React.ChangeEvent) => { + const images = e.target.files; + if (images && images.length === 1) { + const selectedImage = images[0]; + const compressedImage = await CompressImage({ + imageFile: selectedImage, + size: 200, + }); + setAvatar(compressedImage); + const previewUrl = compressedImage + ? URL.createObjectURL(compressedImage) + : ''; + setAvatarPreview(previewUrl); + } + }; + const onAvatarDelete = () => { + setAvatar(null); + setAvatarPreview(null); + }; + + const [name, setName] = useState(initialName); + const onNameChange = (e: React.ChangeEvent) => { + setName(e.target.value); + }; + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!user || isLoading || name === '') return; + try { + setLoading(true); + const userDocRef = doc(db, 'users', user.uid); + await updateDoc(userDocRef, { + userName: name, + }); + await updateProfile(user, { + displayName: name, + }); + const locationRef = ref(storage, `avatars/${user?.uid}`); + if (avatar) { + const result = await uploadBytes(locationRef, avatar); + const url = await getDownloadURL(result.ref); + await updateDoc(userDocRef, { + userAvatar: url, + }); + await updateProfile(user, { + photoURL: url, + }); + } else if (!avatar && initialAvatar && initialAvatar !== avatarPreview) { + await updateProfile(user, { + photoURL: '', + }); + await deleteObject(locationRef); + await updateDoc(userDocRef, { + userAvatar: null, + }); + } + setAvatarPreview(null); + setAvatar(null); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + onClose(); + } + }; + return ( + + {avatarPreview ? ( + <> + + + + ) : ( + + + + )} + + + + {isLoading ? : '수정'} + + + ); } diff --git a/src/components/edit-tweet-form.tsx b/src/components/edit-tweet-form.tsx index ddb9695..a0d0db6 100644 --- a/src/components/edit-tweet-form.tsx +++ b/src/components/edit-tweet-form.tsx @@ -24,13 +24,14 @@ export default function EditTweetForm({ onClose, }: IEditTweetForm) { const [isLoading, setLoading] = useState(false); - const [tweet, setTweet] = useState(initialTweet); - const [image, setImage] = useState(null); - const [imagePreview, setImagePreview] = useState(initialPhoto); - const onChange = (e: React.ChangeEvent) => { + const [tweet, setTweet] = useState(initialTweet); + const onTweetChange = (e: React.ChangeEvent) => { setTweet(e.target.value); }; + + const [image, setImage] = useState(null); + const [imagePreview, setImagePreview] = useState(initialPhoto); const onImageChange = async (e: React.ChangeEvent) => { const images = e.target.files; if (images && images.length === 1) { @@ -50,6 +51,7 @@ export default function EditTweetForm({ setImage(null); setImagePreview(''); }; + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); const user = auth.currentUser; @@ -88,15 +90,21 @@ export default function EditTweetForm({ return ( {imagePreview ? ( <> - + ) : ( @@ -106,8 +114,8 @@ export default function EditTweetForm({ )} diff --git a/src/components/tweet.tsx b/src/components/tweet.tsx index d1b9228..730d928 100644 --- a/src/components/tweet.tsx +++ b/src/components/tweet.tsx @@ -91,7 +91,7 @@ export default function Tweet({ id={id} tweet={tweet} photo={photo} - onClose={() => setEditPopup(false)} + onClose={toggleEditPopup} /> diff --git a/src/components/user-profile.tsx b/src/components/user-profile.tsx index b07bc27..ce75028 100644 --- a/src/components/user-profile.tsx +++ b/src/components/user-profile.tsx @@ -48,7 +48,11 @@ export default function UserProfile() { - + ) : null} diff --git a/src/interfaces/IUser.ts b/src/interfaces/IUser.ts new file mode 100644 index 0000000..0a26000 --- /dev/null +++ b/src/interfaces/IUser.ts @@ -0,0 +1,5 @@ +export default interface IUser { + userId: string; + userName: string; + userAvatar: string; +} diff --git a/src/styles/profile-form.ts b/src/styles/profile-form.ts new file mode 100644 index 0000000..8626b6c --- /dev/null +++ b/src/styles/profile-form.ts @@ -0,0 +1,78 @@ +import React from 'react'; +import { styled } from 'styled-components'; +import { primaryColor, whiteColor } from './global.ts'; +import { Input, SolidButton } from './button.ts'; +import { Avatar } from './profile.ts'; + +export const Form = styled.form` + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const AttachAvatarPreview = styled.img` + width: 120px; + height: 120px; + margin-bottom: 20px; + object-fit: cover; + border-radius: 50%; +`; + +export const AttachAvatarDelete = styled.button` + position: absolute; + top: 0; + right: 70px; + width: 25px; + height: 25px; + background-color: ${primaryColor}; + border: 2px solid ${whiteColor}; + border-radius: 50%; + &::before, + &::after { + content: ''; + position: absolute; + top: 10px; + left: 4px; + width: 13px; + height: 2px; + background-color: ${whiteColor}; + } + &::before { + transform: rotate(45deg); + } + &::after { + transform: rotate(135deg); + } +`; + +export const AttachAvatarButton: React.ComponentType< + React.HTMLProps +> = styled(Avatar).attrs(() => ({ + as: 'label', +}))` + margin-bottom: 20px; + cursor: pointer; + svg, + &::before { + transition: all 0.5s ease; + } + &:hover, + &:active { + svg { + stroke: ${primaryColor}; + } + &::before { + border: 2px dashed ${primaryColor}; + } + } +`; + +export const AttachAvatarInput = styled.input` + display: none; +`; + +export const InputText = styled(Input)``; + +export const SubmitButton = styled(SolidButton)``; diff --git a/src/styles/profile.ts b/src/styles/profile.ts index a1afb71..897e99d 100644 --- a/src/styles/profile.ts +++ b/src/styles/profile.ts @@ -1,5 +1,4 @@ import { styled } from 'styled-components'; -import React from 'react'; import { grayColor } from './global.ts'; import { LineButton } from './button.ts'; @@ -10,7 +9,7 @@ export const Profile = styled.article` justify-content: center; align-items: center; gap: 20px; - padding: 50px 0; + padding: 50px 0 60px; &::after { content: ''; position: absolute; @@ -50,14 +49,6 @@ export const Avatar = styled.div` } `; -export const AvatarUpload: React.ComponentType< - React.HTMLProps -> = styled(Avatar).attrs(() => ({ - as: 'label', -}))` - cursor: pointer; -`; - export const AvatarImage = styled.img` width: 100%; height: 100%; @@ -65,10 +56,6 @@ export const AvatarImage = styled.img` z-index: 10; `; -export const AvatarInput = styled.input` - display: none; -`; - export const Name = styled.h2` font-size: 30px; `; diff --git a/src/styles/tweet-form.ts b/src/styles/tweet-form.ts index a82b072..ad8bd37 100644 --- a/src/styles/tweet-form.ts +++ b/src/styles/tweet-form.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { blackColor, grayColor, primaryColor, whiteColor } from './global.ts'; +import { SolidButton } from './button.ts'; export const Form = styled.form` position: relative; @@ -8,10 +9,10 @@ export const Form = styled.form` flex-shrink: 0; gap: 10px; width: 100%; - padding-bottom: 20px; `; export const PostForm = styled(Form)` + padding-bottom: 10px; &::after { content: ''; position: absolute; @@ -97,7 +98,7 @@ export const AttachImageButton = styled.label` width: 30px; height: 30px; stroke: ${grayColor}; - transition: all 0.3s ease; + transition: all 0.5s ease; } &:hover, &:active { @@ -112,15 +113,4 @@ export const AttachImageInput = styled.input` display: none; `; -export const SubmitButton = styled.button` - background-color: ${primaryColor}; - border: none; - border-radius: 20px; - color: white; - font-size: 16px; - line-height: 36px; - svg { - width: 36px; - height: 36px; - } -`; +export const SubmitButton = styled(SolidButton)``; From a9f0081ed56661a91e187681274505964aa929ce Mon Sep 17 00:00:00 2001 From: sryung Date: Tue, 26 Dec 2023 22:43:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=EB=9D=BC=EC=9D=B8=EC=97=90=20=EA=B8=80=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=85=B8=EC=B6=9C=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/timeline.tsx | 4 +++- src/components/user-timeline.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/timeline.tsx b/src/components/timeline.tsx index d0d290d..8db2636 100644 --- a/src/components/timeline.tsx +++ b/src/components/timeline.tsx @@ -44,11 +44,13 @@ export default function Timeline() { } }; }, []); - return ( + return tweets.length !== 0 ? ( {tweets.map((tweet) => ( ))} + ) : ( + 작성된 글이 없습니다. ); } diff --git a/src/components/user-timeline.tsx b/src/components/user-timeline.tsx index 7fd25be..25444e1 100644 --- a/src/components/user-timeline.tsx +++ b/src/components/user-timeline.tsx @@ -9,9 +9,9 @@ import { where, } from 'firebase/firestore'; import { auth, db } from '../firebase.ts'; +import ITweet from '../interfaces/ITweet.ts'; import Tweet from './tweet.tsx'; import * as S from '../styles/timeline.ts'; -import ITweet from '../interfaces/ITweet.ts'; export default function UserTimeline() { const user = auth.currentUser; @@ -47,11 +47,13 @@ export default function UserTimeline() { } }; }, []); - return ( + return tweets.length !== 0 ? ( {tweets.map((tweet) => ( ))} + ) : ( + 작성된 글이 없습니다. ); } From bdc54d37d74203da3337b0a5615496e89e07cde8 Mon Sep 17 00:00:00 2001 From: sryung Date: Tue, 26 Dec 2023 22:59:19 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20esc=ED=82=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=AB=EA=B8=B0=20=ED=95=A8=EC=88=98=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=ED=9B=84=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=A0=81=EC=9A=A9=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/edit-profile-form.tsx | 2 ++ src/components/edit-tweet-form.tsx | 3 ++- src/components/sign-in.tsx | 15 +++------------ src/components/sign-up.tsx | 15 +++------------ src/utils/use-esc-close.tsx | 17 +++++++++++++++++ 5 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 src/utils/use-esc-close.tsx diff --git a/src/components/edit-profile-form.tsx b/src/components/edit-profile-form.tsx index dd6be9e..2927871 100644 --- a/src/components/edit-profile-form.tsx +++ b/src/components/edit-profile-form.tsx @@ -10,6 +10,7 @@ import { import { auth, db, storage } from '../firebase.ts'; import IUser from '../interfaces/IUser.ts'; import CompressImage from '../utils/compress-image.tsx'; +import useEscClose from '../utils/use-esc-close.tsx'; import * as S from '../styles/profile-form.ts'; import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; @@ -92,6 +93,7 @@ export default function EditProfileForm({ onClose(); } }; + useEscClose(onClose); return ( {avatarPreview ? ( diff --git a/src/components/edit-tweet-form.tsx b/src/components/edit-tweet-form.tsx index a0d0db6..4543d98 100644 --- a/src/components/edit-tweet-form.tsx +++ b/src/components/edit-tweet-form.tsx @@ -9,6 +9,7 @@ import { import { auth, db, storage } from '../firebase.ts'; import ITweet from '../interfaces/ITweet.ts'; import CompressImage from '../utils/compress-image.tsx'; +import useEscClose from '../utils/use-esc-close.tsx'; import * as S from '../styles/tweet-form.ts'; import { ReactComponent as IconPhoto } from '../assets/images/i-photo.svg'; import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; @@ -24,7 +25,6 @@ export default function EditTweetForm({ onClose, }: IEditTweetForm) { const [isLoading, setLoading] = useState(false); - const [tweet, setTweet] = useState(initialTweet); const onTweetChange = (e: React.ChangeEvent) => { setTweet(e.target.value); @@ -87,6 +87,7 @@ export default function EditTweetForm({ onClose(); } }; + useEscClose(onClose); return ( { - const handleEscKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - document.addEventListener('keydown', handleEscKey); - return () => { - document.removeEventListener('keydown', handleEscKey); - }; - }, [onClose]); const onChange = (e: React.ChangeEvent) => { const { target: { name, value }, @@ -62,6 +52,7 @@ export default function SignIn({ onClose }: ISignInProps) { setLoading(false); } }; + useEscClose(onClose); return ( diff --git a/src/components/sign-up.tsx b/src/components/sign-up.tsx index 8845235..34110a4 100644 --- a/src/components/sign-up.tsx +++ b/src/components/sign-up.tsx @@ -1,9 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FirebaseError } from 'firebase/app'; import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth'; import { doc, setDoc } from 'firebase/firestore'; import { auth, db } from '../firebase.ts'; +import useEscClose from '../utils/use-esc-close.tsx'; import * as S from '../styles/auth.ts'; import * as P from '../styles/popup.ts'; import ImageComputer from '../assets/images/logo-small.png'; @@ -27,17 +28,6 @@ export default function SignUp({ onClose }: ISignUpProps) { const [userEmail, setUserEmail] = useState(''); const [userPassword, setUserPassword] = useState(''); const [firebaseError, setFirebaseError] = useState(''); - useEffect(() => { - const handleEscKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - document.addEventListener('keydown', handleEscKey); - return () => { - document.removeEventListener('keydown', handleEscKey); - }; - }, [onClose]); const onChange = (e: React.ChangeEvent) => { const { target: { name, value }, @@ -80,6 +70,7 @@ export default function SignUp({ onClose }: ISignUpProps) { setLoading(false); } }; + useEscClose(onClose); return ( diff --git a/src/utils/use-esc-close.tsx b/src/utils/use-esc-close.tsx new file mode 100644 index 0000000..51b2365 --- /dev/null +++ b/src/utils/use-esc-close.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +const useEscClose = (onClose: () => void) => { + useEffect(() => { + const handleEscKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + document.addEventListener('keydown', handleEscKey); + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [onClose]); +}; + +export default useEscClose;