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

[SP2] 블로그 탭 게시물 목록 구현 #232

Merged
merged 13 commits into from
Oct 23, 2023
3 changes: 3 additions & 0 deletions src/assets/icons/ic_heart_filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/ic_heart_unfilled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/assets/icons/ic_profile_default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions src/lib/types/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type BlogPostListType = {
limit: number;
totalCount: number;
totalPage: number;
currentPage: number;
data: BlogPostType[];
hasNextPage: boolean;
hasPrevPage: boolean;
};

export type BlogPostType = {
id: number;
part: string;
generation: number;
thumbnailUrl: string;
title: string;
description: string;
author: string;
authorProfileImageUrl: string | null;
url: string;
uploadedAt: string;

/* article */
likeCount?: number;
liked?: boolean;

/* review */
subject?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Image from 'next/image';
import icProfileDefault from '@src/assets/icons/ic_profile_default.svg';
import * as S from './style';

function DefaultProfileImage() {
return (
<S.DefaultProfileImage>
<Image src={icProfileDefault} alt="작성자 프로필 이미지" width={10} height={10} />
</S.DefaultProfileImage>
);
}

export default DefaultProfileImage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';

export const DefaultProfileImage = styled.div`
display: flex;
justify-content: center;
align-items: center;

width: 18px;
height: 18px;
border-radius: 18px;
background-color: ${colors.gray700};
`;
39 changes: 39 additions & 0 deletions src/views/BlogPage/components/BlogPost/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BlogPostType } from '@src/lib/types/blog';
import { formatDate } from '@src/lib/utils/dateFormat';
import { DefaultProfileImage } from '@src/views/BlogPage/components/BlogPost';
import * as S from './style';

interface HeaderProps {
selectedTap: string;
blogPost: BlogPostType;
}

function Header({ selectedTap, blogPost }: HeaderProps) {
return (
<S.Header>
{selectedTap === 'ARTICLE' ? (
<>
<S.Profile>
{blogPost.authorProfileImageUrl ? (
<S.ProfileImage
src={blogPost.authorProfileImageUrl}
alt="작성자 프로필 이미지"
width={18}
height={18}
/>
) : (
<DefaultProfileImage />
)}
<div>{blogPost.author}</div>
</S.Profile>
<S.Divider>∙</S.Divider>
<div>{formatDate(new Date(blogPost.uploadedAt), 'yyyymmdd', '.')}</div>
</>
) : (
<div>{blogPost.subject}</div>
)}
</S.Header>
);
}

export default Header;
46 changes: 46 additions & 0 deletions src/views/BlogPage/components/BlogPost/Header/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';
import Image from 'next/image';

export const Header = styled.div`
display: flex;
height: 23px;
margin-bottom: 4px;

color: ${colors.gray200};
font-size: 14px;
font-weight: 400;
line-height: 160%;
letter-spacing: -0.21px;

/* 모바일 뷰 */
@media (max-width: 767px) {
height: 16px;
margin-bottom: 0;

font-size: 12px;
font-weight: 500;
line-height: 135%; /* 16.2px */
letter-spacing: -0.18px;
}
`;

export const Profile = styled.div`
display: flex;
align-items: center;
gap: 6px;
`;

export const ProfileImage = styled(Image)`
border-radius: 18px;

/* 모바일 뷰 */
@media (max-width: 767px) {
width: 15px;
height: 15px;
}
`;

export const Divider = styled.div`
padding: 0 2px 0 2px;
`;
25 changes: 25 additions & 0 deletions src/views/BlogPage/components/BlogPost/Like/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Image from 'next/image';
import icHeartFilled from '@src/assets/icons/ic_heart_filled.svg';
import icHeartUnfilled from '@src/assets/icons/ic_heart_unfilled.svg';
import { BlogPostType } from '@src/lib/types/blog';
import * as S from './style';

interface LikeProps {
blogPost: BlogPostType;
}

function Like({ blogPost }: LikeProps) {
return (
<S.Like>
<Image
src={blogPost.liked ? icHeartFilled : icHeartUnfilled}
alt="좋아요"
width={16}
height={16}
/>
<span>{blogPost.likeCount}</span>
</S.Like>
);
}

export default Like;
34 changes: 34 additions & 0 deletions src/views/BlogPage/components/BlogPost/Like/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';

export const Like = styled.div`
display: flex;
justify-content: center;
align-items: center;
gap: 3px;

position: absolute;
top: 10px;
right: 10px;

height: 28px;
padding: 2px 8px;
border-radius: 6px;
background: ${colors.grayAlpha100};

color: ${colors.gray100};
font-size: 14px;
font-weight: 400;
line-height: 160%; /* 22.4px */
letter-spacing: -0.21px;

transition: opacity 0.1s ease-out;
&:hover {
opacity: 0.8;
}

/* 모바일 뷰 */
@media (max-width: 767px) {
display: none;
}
`;
3 changes: 3 additions & 0 deletions src/views/BlogPage/components/BlogPost/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as DefaultProfileImage } from './DefaultProfileImage';
export { default as Header } from './Header';
export { default as Like } from './Like';
47 changes: 47 additions & 0 deletions src/views/BlogPage/components/BlogPost/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useRef, useState } from 'react';
import type { BlogPostType } from '@src/lib/types/blog';
import { parsePartToKorean } from '@src/lib/utils/parsePartToKorean';
import { Like } from '@src/views/BlogPage/components/BlogPost';
import * as S from './style';
import Header from '@src/views/BlogPage/components/BlogPost/Header';

const TWO_LINE_TITLE_HEIGHT = 72;

interface BlogPostProps {
selectedTap: string;
blogPost: BlogPostType;
}

function BlogPost({ selectedTap, blogPost }: BlogPostProps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion;

저희 컨벤션에 export default function~ 으로 작업하기로 되어있습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 꼼꼼하게 봐주셔서 감사합니다!!

const titleRef = useRef<HTMLDivElement>(null);
const [descriptionLine, setDescriptionLine] = useState(1);

useEffect(() => {
if (titleRef.current) {
const titleHeight = titleRef.current.clientHeight;
setDescriptionLine(titleHeight >= TWO_LINE_TITLE_HEIGHT ? 1 : 2);
}
}, []);

return (
<S.BlogPost href={blogPost.url}>
<div>
<Header selectedTap={selectedTap} blogPost={blogPost} />
<S.Body>
<S.Title ref={titleRef}>{blogPost.title}</S.Title>
<S.Description descriptionLine={descriptionLine}>{blogPost.description}</S.Description>
</S.Body>
<S.TagList>
<S.Tag>{blogPost.generation}기</S.Tag>
<S.Tag>{parsePartToKorean(blogPost.part)}</S.Tag>
</S.TagList>
</div>
<S.ThumbnailWrapper>
<S.Thumbnail src={blogPost.thumbnailUrl} alt="게시물 썸네일" width={239} height={160} />
{selectedTap === 'ARTICLE' && <Like blogPost={blogPost} />}
</S.ThumbnailWrapper>
</S.BlogPost>
);
}

export default BlogPost;
Loading
Loading