[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 11 'post.tsx'
2024. 7. 8. 18:51ㆍ웹개발 포트폴리오
728x90
반응형
post.tsx
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { auth, db } from '../firebase';
import { doc, getDoc, updateDoc } from 'firebase/firestore';
import { Timestamp } from "@firebase/firestore";
import { Link } from 'react-router-dom';
import Modal, { Styles } from "react-modal";
import { media } from '../media-query-file';
// 날짜를 표시하는 스타일드 컴포넌트
const DateSpan = styled.span`
display: block;
font-family: pretendard-regular;
color: #8E88A0;
float: right;
width: 100%;
margin-top: 15px;
`;
// 트윗 카드의 래퍼 스타일드 컴포넌트
const Wrapper = styled.div`
display: grid;
grid-template-columns: auto 1fr auto;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 15px;
align-items: center;
max-width: 1200px;
padding: 20px;
${media.mobile(`
body {
padding: 0 10px !important;
}
`)}
${media.tablet(`
body {
padding: 0 30px !important;
}
`)}
${media.desktop(`
body {
padding: 0 50px !important;
`)}
`;
// 컬럼 스타일드 컴포넌트
const Column = styled.div`
&:last-child {
place-self: first;
}
color: black;
width: 100%;
`;
// 사진 스타일드 컴포넌트
const Photo = styled.img`
width: 100px;
height: 100px;
border-radius: 15px;
object-fit: cover;
`;
// 프로필 사진 링크 스타일드 컴포넌트
const ProfilePhoto = styled(Link)`
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
background-size: cover;
background-position: center;
margin-right: 50px;
`;
// 사용자 이름 스타일드 컴포넌트
const Username = styled.span`
font-weight: 600;
font-family: pretendard-regular;
color: black;
`;
// 트윗 내용 스타일드 컴포넌트
const Payload = styled.p`
margin: 10px 0px;
font-family: pretendard-regular;
color: black;
width: 100%;
word-break: break-word;
white-space: pre-wrap;
`;
// 사진 그리드 스타일드 컴포넌트
const PhotoGrid = styled.div`
display: flex;
margin : 15px 0;
gap: 10px;
flex-wrap: wrap;
`;
// 삭제 버튼 스타일드 컴포넌트
const DeleteButton = styled.button`
font-family: pretendard-regular;
background-color: #f5957f;
color: white;
border: 0;
padding: 5px 10px;
text-transform: uppercase;
border-radius: 5px;
cursor: pointer;
margin-top: 15px;
@media (max-width: 768px) {
padding: 5px 7px;
font-size: 1.2vw;
}
@media (min-width: 769px) {
padding: 5px 7.5px;
font-size: 1.0vw;
}
`;
// 좋아요 버튼 스타일드 컴포넌트
const LikeButton = styled.button<{ disabled?: boolean }>`
font-family: pretendard-regular;
font-size: 1.0vw;
background-color: ${props => props.disabled ? '#ccc' : '#ff5a5f'};
color: white;
border: 0;
padding: 5px 10px;
border-radius: 5px;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
margin-top: 10px;
@media (max-width: 768px) {
padding: 5px 7px;
font-size: 1.2vw;
}
@media (min-width: 769px) {
padding: 5px 7.5px;
font-size: 1.0vw;
}
&:hover {
background-color: ${props => props.disabled ? '#ccc' : '#e04848'};
}
`;
// 좋아요 수를 표시하는 스타일드 컴포넌트
const LikesCount = styled.span`
margin-left: 10px;
`;
// 모달 스타일 설정
const customStyles: Styles = {
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
content: {
top: 'auto',
left: 'auto',
right: 'auto',
bottom: 'auto',
margin: '0 auto',
width: '90vw',
height: '90vh',
backgroundColor: '#fff',
borderRadius: '10px',
padding: '20px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
overflow: 'hidden',
},
};
// 모달 안의 이미지 스타일드 컴포넌트
const ModalImage = styled.img`
width: 100%;
height: auto;
max-width: 100%;
max-height: 100%;
object-fit: contain; // 이미지의 비율을 유지하면서 컨테이너에 맞추기
`;
// 닫기 버튼 스타일드 컴포넌트
const CloseButton = styled.button`
background-color: none;
color: #8F89A1;
border: none;
border-radius: 5px;
cursor: pointer;
position: absolute; // 추가
top: 10px; // 추가
right: 10px; // 추가
`;
// 트윗 컴포넌트 인터페이스
export interface TweetProps {
id: string;
photos?: string[];
tweet: string;
userId: string;
username: string;
profileImage: string;
createdAt: Timestamp;
onDeleteTweet: (id: string) => void;
}
// 트윗 컴포넌트 정의
const Tweet: React.FC<TweetProps> = ({ username, photos = [], tweet, userId, id, profileImage, createdAt, onDeleteTweet }) => {
const [likes, setLikes] = useState(0); // 좋아요 수 상태
const [modalIsOpen, setIsOpen] = useState(false); // 모달 열림 상태
const [selectedImage, setSelectedImage] = useState<string | null>(null); // 선택된 이미지 상태
const currentUserId = auth.currentUser?.uid; // 현재 로그인한 사용자 ID
// 좋아요 수 가져오기
const fetchLikes = async () => {
try {
const docRef = doc(db, 'tweets', id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data();
setLikes(data.likes || 0);
}
} catch (e) {
console.error('Error fetching likes: ', e);
}
};
// 컴포넌트 마운트 시 좋아요 수 가져오기
useEffect(() => {
fetchLikes();
}, []);
// 좋아요 버튼 클릭 핸들러
const handleLike = async () => {
if (currentUserId === userId) return; // 작성자와 로그인한 사용자가 같으면 좋아요를 누를 수 없도록 함
const newLikes = likes + 1;
setLikes(newLikes);
try {
const docRef = doc(db, 'tweets', id);
await updateDoc(docRef, {
likes: newLikes,
});
} catch (e) {
console.error('Error updating likes: ', e);
}
};
// 모달 열기
const openModal = (image: string) => {
setSelectedImage(image);
setIsOpen(true);
};
// 모달 닫기
const closeModal = () => {
setIsOpen(false);
setSelectedImage(null);
};
// 날짜 포맷팅 함수
const formatDate = (timestamp: Timestamp) => {
const date = timestamp.toDate();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
return (
<Wrapper>
<ProfilePhoto to={`/profile/${userId}`} style={{ backgroundImage: `url(${profileImage})` }} />
<Column>
<Username>{username}</Username>
{photos.length > 0 && (
<PhotoGrid>
{photos.map((photo, index) => (
<Photo key={index} src={photo} onClick={() => openModal(photo)} />
))}
</PhotoGrid>
)}
<Payload>{tweet}</Payload>
<LikeButton onClick={handleLike} disabled={currentUserId === userId}>
UP
<LikesCount>{likes}</LikesCount>
</LikeButton>
{auth.currentUser?.uid === userId && (
<DeleteButton onClick={() => onDeleteTweet(id)}>지우기</DeleteButton>
)}
<DateSpan>{formatDate(createdAt)}</DateSpan>
</Column>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={customStyles}
>
<CloseButton onClick={closeModal}>X</CloseButton>
{selectedImage && <ModalImage src={selectedImage} />}
</Modal>
</Wrapper>
);
};
export default Tweet;
위 코드는 포스트를 표시하는 컴포넌트입니다. 주요 기능은 다음과 같습니다:
- 포스트 정보 표시: username, photos, tweet, userId, profileImage, createdAt 등의 정보를 표시합니다.
- UP 기능: 사용자가 포스트에 UP을 누를 수 있습니다. handleLike 함수가 이를 처리합니다.
- 모달을 통한 이미지 확대 보기: 포스트에 포함된 이미지를 클릭하면 모달이 열리며 확대된 이미지를 볼 수 있습니다.
- 포스트 삭제 기능: 사용자가 자신의 포스트를 삭제할 수 있습니다. onDeleteTweet 함수가 이를 처리합니다.
- 스타일링: 여러 styled-components를 사용하여 포스트 카드, 버튼, 이미지, 텍스트 등의 스타일을 정의합니다.
728x90
반응형
'웹개발 포트폴리오' 카테고리의 다른 글
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 13 'timeline.tsx' (0) | 2024.07.08 |
---|---|
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 12 'post-tweet-form.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 10 'profile.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 9 'create-account.tsx' & 'login.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 8 'home.tsx' (0) | 2024.07.08 |