Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

프로필 수정하기 기능 업그레이드 (#5 #9) & 공통 개편사항 적용 (#17 #18) #19

Merged
merged 4 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/components/edit-profile-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 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';

interface IEditProfileForm extends Pick<IUser, 'userAvatar' | 'userName'> {
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<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState(user?.photoURL);
const onAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
setName(e.target.value);
};

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
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();
}
};
useEscClose(onClose);
return (
<S.Form onSubmit={onSubmit}>
{avatarPreview ? (
<>
<S.AttachAvatarPreview
src={avatarPreview}
alt="프로필이미지 미리보기"
width="120"
height="120"
/>
<S.AttachAvatarDelete type="button" onClick={onAvatarDelete} />
</>
) : (
<S.AttachAvatarButton htmlFor="avatar_edit">
<IconUser />
</S.AttachAvatarButton>
)}
<S.AttachAvatarInput
onChange={onAvatarChange}
id="avatar_edit"
type="file"
accept="image/*"
/>
<S.InputText
onChange={onNameChange}
name="name_edit"
type="text"
placeholder="이름을 입력해주세요"
value={name}
required
/>
<S.SubmitButton type="submit">
{isLoading ? <LoadingSpinner /> : '수정'}
</S.SubmitButton>
</S.Form>
);
}
23 changes: 16 additions & 7 deletions src/components/edit-tweet-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,12 +26,12 @@ export default function EditTweetForm({
}: IEditTweetForm) {
const [isLoading, setLoading] = useState(false);
const [tweet, setTweet] = useState(initialTweet);
const [image, setImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState(initialPhoto);

const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const onTweetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTweet(e.target.value);
};

const [image, setImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState(initialPhoto);
const onImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const images = e.target.files;
if (images && images.length === 1) {
Expand All @@ -50,6 +51,7 @@ export default function EditTweetForm({
setImage(null);
setImagePreview('');
};

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const user = auth.currentUser;
Expand Down Expand Up @@ -85,18 +87,25 @@ export default function EditTweetForm({
onClose();
}
};
useEscClose(onClose);
return (
<S.EditForm onSubmit={onSubmit}>
<S.TextArea
onChange={onChange}
onChange={onTweetChange}
value={tweet}
rows={5}
maxLength={180}
placeholder="지금 무슨 일이 일어나고 있나요?"
required
/>
{imagePreview ? (
<>
<S.AttachImagePreview src={imagePreview} alt="첨부이미지 미리보기" />
<S.AttachImagePreview
src={imagePreview}
alt="첨부이미지 미리보기"
width="120"
height="120"
/>
<S.AttachImageDelete type="button" onClick={onImageDelete} />
</>
) : (
Expand All @@ -106,8 +115,8 @@ export default function EditTweetForm({
)}
<S.AttachImageInput
onChange={onImageChange}
type="file"
id="image_edit"
type="file"
accept="image/*"
/>
<S.SubmitButton type="submit">
Expand Down
15 changes: 3 additions & 12 deletions src/components/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FirebaseError } from 'firebase/app';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } 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';
Expand All @@ -25,17 +26,6 @@ export default function SignIn({ onClose }: ISignInProps) {
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<HTMLInputElement>) => {
const {
target: { name, value },
Expand All @@ -62,6 +52,7 @@ export default function SignIn({ onClose }: ISignInProps) {
setLoading(false);
}
};
useEscClose(onClose);
return (
<P.PopupWrapper>
<P.Popup>
Expand Down
15 changes: 3 additions & 12 deletions src/components/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLInputElement>) => {
const {
target: { name, value },
Expand Down Expand Up @@ -80,6 +70,7 @@ export default function SignUp({ onClose }: ISignUpProps) {
setLoading(false);
}
};
useEscClose(onClose);
return (
<P.PopupWrapper>
<P.Popup>
Expand Down
4 changes: 3 additions & 1 deletion src/components/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ export default function Timeline() {
}
};
}, []);
return (
return tweets.length !== 0 ? (
<S.TimelineWrapper>
{tweets.map((tweet) => (
<Tweet key={tweet.id} {...tweet} />
))}
</S.TimelineWrapper>
) : (
<S.Text>작성된 글이 없습니다.</S.Text>
);
}
2 changes: 1 addition & 1 deletion src/components/tweet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default function Tweet({
id={id}
tweet={tweet}
photo={photo}
onClose={() => setEditPopup(false)}
onClose={toggleEditPopup}
/>
</P.Popup>
</P.PopupWrapper>
Expand Down
61 changes: 61 additions & 0 deletions src/components/user-profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 (
<S.Profile>
<S.Avatar>
{userAvatar ? (
<S.AvatarImage
src={userAvatar}
alt="프로필 이미지"
width="120"
height="120"
/>
) : (
<IconUser />
)}
</S.Avatar>
<S.Name>{userName}</S.Name>
<S.EditButton onClick={toggleEditPopup} type="button">
프로필 수정
</S.EditButton>
{editPopup ? (
<P.PopupWrapper>
<P.Popup>
<P.CloseButton onClick={toggleEditPopup} type="button" />
<EditProfileForm
userAvatar={userAvatar}
userName={userName}
onClose={toggleEditPopup}
/>
</P.Popup>
</P.PopupWrapper>
) : null}
</S.Profile>
);
}
6 changes: 4 additions & 2 deletions src/components/user-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,11 +47,13 @@ export default function UserTimeline() {
}
};
}, []);
return (
return tweets.length !== 0 ? (
<S.TimelineWrapper>
{tweets.map((tweet) => (
<Tweet key={tweet.id} {...tweet} />
))}
</S.TimelineWrapper>
) : (
<S.Text>작성된 글이 없습니다.</S.Text>
);
}
5 changes: 5 additions & 0 deletions src/interfaces/IUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default interface IUser {
userId: string;
userName: string;
userAvatar: string;
}
Loading
Loading