diff --git a/src/components/signUp.tsx b/src/components/signUp.tsx index 5ea8d0b..4007a09 100644 --- a/src/components/signUp.tsx +++ b/src/components/signUp.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FirebaseError } from 'firebase/app'; import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth'; -import { auth } from '../firebase.ts'; +import { doc, setDoc } from 'firebase/firestore'; +import { auth, db } from '../firebase.ts'; import * as S from '../styles/auth.ts'; import * as P from '../styles/popup.ts'; import ImageComputer from '../assets/images/logo-small.png'; @@ -63,6 +64,12 @@ export default function SignUp({ onClose }: ISignUpProps) { await updateProfile(credentials.user, { displayName: userName, }); + const userRef = doc(db, 'users', credentials.user.uid); + await setDoc(userRef, { + userName: userName || 'Anonymous', + userId: credentials.user.uid, + userAvatar: credentials.user.photoURL || null, + }); navigate('/'); } catch (error) { if (error instanceof FirebaseError) { diff --git a/src/components/tweet.tsx b/src/components/tweet.tsx index 25dfe3d..d6117d3 100644 --- a/src/components/tweet.tsx +++ b/src/components/tweet.tsx @@ -1,11 +1,12 @@ -import { useState } from 'react'; -import { deleteDoc, doc } from 'firebase/firestore'; +import { useEffect, useState } from 'react'; +import { deleteDoc, doc, getDoc } from 'firebase/firestore'; import { deleteObject, ref } from 'firebase/storage'; import { auth, db, storage } from '../firebase.ts'; import ITweet from '../interfaces/ITweet.ts'; import FormattedDate from '../utils/formattedDate.tsx'; import * as S from '../styles/tweet.ts'; import * as P from '../styles/popup.ts'; +import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; export default function Tweet({ id, @@ -16,6 +17,12 @@ export default function Tweet({ tweet, }: ITweet) { const user = auth.currentUser; + const [userAvatar, setUserAvatar] = useState(null); + const fetchUserAvatar = async () => { + const userDoc = await getDoc(doc(db, 'users', userId)); + const userData = userDoc.data(); + setUserAvatar(userData?.userAvatar || null); + }; const [deletePopup, setDeletePopup] = useState(false); const toggleDeletePopup = () => { setDeletePopup(!deletePopup); @@ -34,20 +41,26 @@ export default function Tweet({ // } }; + useEffect(() => { + fetchUserAvatar(); + }, [userId, userAvatar]); return ( - - + + + {userAvatar ? : } + + {userName} {FormattedDate(createdAt)} - {tweet} - {user?.uid === userId ? ( - 삭제 - ) : null} - - {photo ? ( - - - + + {tweet} + {photo ? : null} + {user?.uid === userId ? ( + <> + + 포스팅 삭제하기 + + ) : null} {deletePopup ? ( diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index fe441a7..c7ec994 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; import { updateProfile } from 'firebase/auth'; -import { auth, storage } from '../firebase.ts'; +import { doc, updateDoc } from 'firebase/firestore'; +import { auth, db, storage } from '../firebase.ts'; import WindowTop from '../components/window-top.tsx'; import UserTimeline from '../components/user-timeline.tsx'; import * as W from '../styles/window.ts'; @@ -11,6 +12,12 @@ import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; 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 { files } = e.target; if (!user) return; @@ -23,6 +30,7 @@ export default function Profile() { await updateProfile(user, { photoURL: avatarUrl, }); + await updateUserAvatar(user.uid, avatarUrl); } }; return ( diff --git a/src/styles/menu.ts b/src/styles/menu.ts index 19f4926..ffd9779 100644 --- a/src/styles/menu.ts +++ b/src/styles/menu.ts @@ -16,7 +16,7 @@ export const Logo = styled.div` export const LogoTitle = styled.h1` color: ${whiteColor}; - font-size: 30px; + font-size: 34px; text-shadow: ${LogoTextShadow(2)}; span { color: ${primaryColor}; diff --git a/src/styles/popup.ts b/src/styles/popup.ts index a68e428..d663660 100644 --- a/src/styles/popup.ts +++ b/src/styles/popup.ts @@ -20,34 +20,29 @@ export const PopupWrapper = styled.div` background-color: rgba(0, 0, 0, 0.4); `; -export const Popup = styled.div` +export const PopupBox = styled.div` position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; width: calc(100vw - 100px); + padding: 30px; + background-color: ${whiteColor}; + border: 3px solid ${blackColor}; +`; + +export const Popup = styled(PopupBox)` max-width: 757px; height: calc(100vh - 100px); max-height: 728px; - padding: 30px; - background-color: ${whiteColor}; border-radius: 10px; `; -export const MiniPopup = styled.div` - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: calc(100vw - 100px); +export const MiniPopup = styled(PopupBox)` max-width: 400px; height: auto; - padding: 30px; - background-color: ${whiteColor}; border-radius: 6px; - border: 3px solid ${blackColor}; `; export const CloseButton = styled.button` diff --git a/src/styles/post-tweet-form.ts b/src/styles/post-tweet-form.ts index 800854a..2fa8165 100644 --- a/src/styles/post-tweet-form.ts +++ b/src/styles/post-tweet-form.ts @@ -8,7 +8,7 @@ export const Form = styled.form` flex-shrink: 0; gap: 10px; width: 100%; - padding-bottom: 10px; + padding-bottom: 20px; &::after { content: ''; position: absolute; diff --git a/src/styles/profile.ts b/src/styles/profile.ts index 61dd390..56580a7 100644 --- a/src/styles/profile.ts +++ b/src/styles/profile.ts @@ -2,12 +2,23 @@ import { styled } from 'styled-components'; import { grayColor } from './global.ts'; export const Avatar = styled.div` + position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 20px; - margin: 50px 0; + padding: 50px 0; + &::after { + content: ''; + position: absolute; + bottom: 0; + left: -10px; + right: -10px; + width: calc(100% + 20px); + height: 2px; + background-color: #070707; + } `; export const AvatarUpload = styled.label` diff --git a/src/styles/timeline.ts b/src/styles/timeline.ts index 5ecd51b..5202b05 100644 --- a/src/styles/timeline.ts +++ b/src/styles/timeline.ts @@ -2,6 +2,7 @@ import styled from 'styled-components'; export const TimelineWrapper = styled.ul` display: block; + margin-top: 10px; overflow-y: auto; `; diff --git a/src/styles/tweet.ts b/src/styles/tweet.ts index 95459b2..98f4a64 100644 --- a/src/styles/tweet.ts +++ b/src/styles/tweet.ts @@ -1,26 +1,53 @@ import { styled } from 'styled-components'; -import { grayColor, primaryColor, whiteColor } from './global.ts'; +import { grayColor, primaryColor } from './global.ts'; -interface IWrapperProps { - hasPhoto: boolean; -} - -export const Wrapper = styled.li` - display: grid; - grid-template-columns: ${({ hasPhoto }) => (hasPhoto ? '3fr 1fr' : '1fr')}; - padding: 20px; - margin: 20px 0; +export const Wrapper = styled.li` + position: relative; + display: flex; + flex-direction: column; + padding: 20px 20px 20px 60px; &:not(:last-child) { border-bottom: 1px dashed ${grayColor}; } `; -export const Column = styled.div``; +export const Row = styled.div``; -export const Photo = styled.img` - width: 100px; - height: 100px; - border-radius: 15px; +export const Avatar = styled.div` + position: absolute; + top: 20px; + left: 10px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + width: 40px; + height: 40px; + border-radius: 50%; + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 36px; + height: 36px; + border: 2px dashed ${grayColor}; + border-radius: 50%; + } + svg { + width: 26px; + height: 26px; + stroke: ${grayColor}; + } +`; + +export const AvatarImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + z-index: 10; `; export const Username = styled.span` @@ -39,11 +66,49 @@ export const Payload = styled.p` font-size: 18px; `; +export const Photo = styled.img` + width: 300px; + height: 300px; + border-radius: 15px; +`; + export const DeleteButton = styled.button` - padding: 5px 10px; - background-color: ${primaryColor}; - border: 0; - border-radius: 5px; - color: ${whiteColor}; - font-size: 12px; + position: absolute; + top: 16px; + right: 20px; + width: 25px; + height: 25px; + background-color: transparent; + border: none; + &::before, + &::after { + content: ''; + position: absolute; + top: 11px; + left: 6px; + width: 13px; + height: 2px; + background-color: ${primaryColor}; + } + &::before { + transform: rotate(45deg); + } + &::after { + transform: rotate(135deg); + } +`; + +export const EditButton = styled.button` + position: absolute; + top: 16px; + right: 50px; + width: 25px; + height: 25px; + background-color: transparent; + border: none; + svg { + width: 16px; + height: 16px; + stroke: ${primaryColor}; + } `;