From 8ac0aa86769a5702968c966bbde90550deb84daa Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 26 Feb 2020 00:01:47 +0900 Subject: [PATCH 1/8] Initialize UI for PostCard Legacy PostCard is renamed to FlatPostCard --- src/App.tsx | 8 +- src/components/common/FlatPostCard.tsx | 299 ++++++++++++++ ...{PostCardList.tsx => FlatPostCardList.tsx} | 2 +- src/components/common/PostCard.tsx | 379 +++++------------- src/components/common/PostCardGrid.tsx | 28 ++ src/components/home/HomeHeader.tsx | 40 ++ src/components/home/HomeLayout.tsx | 30 ++ src/components/home/HomeResponsive.tsx | 19 + src/components/home/HomeTab.tsx | 77 ++++ src/components/home/HomeTemplate.tsx | 27 ++ src/containers/main/RecentPosts.tsx | 2 +- src/containers/main/TrendingPosts.tsx | 2 +- src/containers/search/SearchResult.tsx | 2 +- src/containers/tags/TagDetailContainer.tsx | 2 +- src/containers/velog/UserPosts.tsx | 2 +- src/lib/graphql/post.ts | 2 + src/pages/home/HomePage.tsx | 37 ++ src/pages/home/RecentPostsPage.tsx | 9 + src/pages/home/TrendingPostsPage.tsx | 13 + src/pages/home/hooks/useTrendingPosts.ts | 13 + 20 files changed, 710 insertions(+), 283 deletions(-) create mode 100644 src/components/common/FlatPostCard.tsx rename src/components/common/{PostCardList.tsx => FlatPostCardList.tsx} (94%) create mode 100644 src/components/common/PostCardGrid.tsx create mode 100644 src/components/home/HomeHeader.tsx create mode 100644 src/components/home/HomeLayout.tsx create mode 100644 src/components/home/HomeResponsive.tsx create mode 100644 src/components/home/HomeTab.tsx create mode 100644 src/components/home/HomeTemplate.tsx create mode 100644 src/pages/home/HomePage.tsx create mode 100644 src/pages/home/RecentPostsPage.tsx create mode 100644 src/pages/home/TrendingPostsPage.tsx create mode 100644 src/pages/home/hooks/useTrendingPosts.ts diff --git a/src/App.tsx b/src/App.tsx index 84d8e42d..d0346fd3 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import VelogPageFallback from './containers/velog/VelogPageFallback'; import ErrorBoundary from './components/error/ErrorBoundary'; import NotFoundPage from './pages/NotFoundPage'; import { Helmet } from 'react-helmet-async'; +import HomePage from './pages/home/HomePage'; const loadableConfig = { fallback: , @@ -57,12 +58,13 @@ const App: React.FC = props => { - - + + + {/* */} diff --git a/src/components/common/FlatPostCard.tsx b/src/components/common/FlatPostCard.tsx new file mode 100644 index 00000000..4a0a1e5b --- /dev/null +++ b/src/components/common/FlatPostCard.tsx @@ -0,0 +1,299 @@ +import React, { useRef } from 'react'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { userThumbnail } from '../../static/images'; +import Tag from './TagItem'; +import { PartialPost } from '../../lib/graphql/post'; +import { formatDate } from '../../lib/utils'; +import usePrefetchPost from '../../lib/hooks/usePrefetchPost'; +import Skeleton from './Skeleton'; +import SkeletonTexts from './SkeletonTexts'; +import RatioImage from './RatioImage'; +import media from '../../lib/styles/media'; +import PrivatePostLabel from './PrivatePostLabel'; +import optimizeImage from '../../lib/optimizeImage'; + +const PostCardBlock = styled.div` + padding-top: 4rem; + padding-bottom: 4rem; + ${media.small} { + padding-top: 2rem; + padding-bottom: 2rem; + } + + & > a { + color: inherit; + text-decoration: none; + } + &:first-child { + padding-top: 0; + } + .user-info { + display: flex; + align-items: center; + img { + width: 3rem; + height: 3rem; + display: block; + margin-right: 1rem; + background: ${palette.gray0}; + object-fit: cover; + border-radius: 1.5rem; + box-shadow: 0px 0 8px rgba(0, 0, 0, 0.1); + ${media.small} { + width: 2rem; + height: 2rem; + border-radius: 1rem; + } + } + .username { + font-size: 0.875rem; + color: ${palette.gray9}; + font-weight: bold; + a { + color: inherit; + text-decoration: none; + &:hover { + color: ${palette.gray8}; + } + } + } + margin-bottom: 1.5rem; + ${media.small} { + margin-bottom: 0.75rem; + } + } + .post-thumbnail { + margin-bottom: 1rem; + ${media.small} { + } + } + line-height: 1.5; + h2 { + font-size: 1.5rem; + margin: 0; + color: ${palette.gray9}; + word-break: keep-all; + ${media.small} { + font-size: 1rem; + } + } + p { + margin-bottom: 2rem; + margin-top: 0.5rem; + font-size: 1rem; + color: ${palette.gray7}; + word-break: keep-all; + overflow-wrap: break-word; + ${media.small} { + font-size: 0.875rem; + margin-bottom: 1.5rem; + } + } + .subinfo { + display: flex; + align-items: center; + margin-top: 1rem; + color: ${palette.gray6}; + font-size: 0.875rem; + ${media.small} { + font-size: 0.75rem; + } + span { + } + .separator { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + } + .tags-wrapper { + margin-bottom: -0.875rem; + ${media.small} { + margin-bottom: -0.5rem; + } + } + + & + & { + border-top: 1px solid ${palette.gray2}; + } +`; + +interface PostCardProps { + post: PartialPost; + hideUser?: boolean; +} + +const FlatPostCard = ({ post, hideUser }: PostCardProps) => { + const prefetch = usePrefetchPost(post.user.username, post.url_slug); + const prefetchTimeoutId = useRef(null); + + const onMouseEnter = () => { + prefetchTimeoutId.current = setTimeout(prefetch, 2000); + }; + + const onMouseLeave = () => { + if (prefetchTimeoutId.current) { + clearTimeout(prefetchTimeoutId.current); + } + }; + + const url = `/@${post.user.username}/${post.url_slug}`; + const velogUrl = `/@${post.user.username}`; + + if (!post.user.profile) { + console.log(post); + } + return ( + + {!hideUser && ( +
+ + thumbnail + +
+ {post.user.username} +
+
+ )} + {post.thumbnail && ( + + + + )} + +

{post.title}

+ +

{post.short_description}

+
+ {post.tags.map(tag => ( + + ))} +
+
+ {formatDate(post.released_at)} +
·
+ {post.comments_count}개의 댓글 + {post.is_private && ( + <> +
·
+ + + + + )} +
+
+ ); +}; + +export type PostCardSkeletonProps = { + hideUser?: boolean; +}; + +export function PostCardSkeleton({ hideUser }: PostCardSkeletonProps) { + return ( + + {!hideUser && ( +
+ +
+ +
+
+ )} +
+
+ +
+
+

+ +

+
+
+ +
+
+ +
+
+ +
+
+
+ + + +
+
+ + +
+
+ ); +} + +const SkeletonBlock = styled(PostCardBlock)` + h2 { + display: flex; + margin-top: 1.375rem; + margin-bottom: 0.375rem; + } + .user-thumbnail-skeleton { + width: 3rem; + height: 3rem; + ${media.small} { + width: 2rem; + height: 2rem; + } + } + .thumbnail-skeleton-wrapper { + width: 100%; + padding-top: 52.35%; + position: relative; + .skeleton { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + .short-description { + margin-bottom: 2rem; + margin-top: 1rem; + font-size: 1rem; + .line { + display: flex; + } + .line + .line { + margin-top: 0.5rem; + } + } + .tags-skeleton { + line-height: 1; + font-size: 2rem; + ${media.small} { + font-size: 1.25rem; + } + } +`; + +export default React.memo(FlatPostCard); diff --git a/src/components/common/PostCardList.tsx b/src/components/common/FlatPostCardList.tsx similarity index 94% rename from src/components/common/PostCardList.tsx rename to src/components/common/FlatPostCardList.tsx index 6c3e047b..4d6cc919 100644 --- a/src/components/common/PostCardList.tsx +++ b/src/components/common/FlatPostCardList.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styled from 'styled-components'; -import PostCard, { PostCardSkeleton } from './PostCard'; +import PostCard, { PostCardSkeleton } from './FlatPostCard'; import { PartialPost } from '../../lib/graphql/post'; import palette from '../../lib/styles/palette'; diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index d75b0ebb..97ec8089 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -1,299 +1,130 @@ -import React, { useRef } from 'react'; -import { Link } from 'react-router-dom'; +import React from 'react'; import styled from 'styled-components'; -import palette from '../../lib/styles/palette'; -import { userThumbnail } from '../../static/images'; -import Tag from './TagItem'; -import { PartialPost } from '../../lib/graphql/post'; -import { formatDate } from '../../lib/utils'; -import usePrefetchPost from '../../lib/hooks/usePrefetchPost'; -import Skeleton from './Skeleton'; -import SkeletonTexts from './SkeletonTexts'; import RatioImage from './RatioImage'; -import media from '../../lib/styles/media'; -import PrivatePostLabel from './PrivatePostLabel'; -import optimizeImage from '../../lib/optimizeImage'; +import { ellipsis } from '../../lib/styles/utils'; +import palette from '../../lib/styles/palette'; +import { LikeIcon } from '../../static/svg'; -const PostCardBlock = styled.div` - padding-top: 4rem; - padding-bottom: 4rem; - ${media.small} { - padding-top: 2rem; - padding-bottom: 2rem; - } +export type PostCardProps = {}; - & > a { - color: inherit; - text-decoration: none; - } - &:first-child { - padding-top: 0; - } - .user-info { - display: flex; - align-items: center; - img { - width: 3rem; - height: 3rem; - display: block; - margin-right: 1rem; - background: ${palette.gray0}; - object-fit: cover; - border-radius: 1.5rem; - box-shadow: 0px 0 8px rgba(0, 0, 0, 0.1); - ${media.small} { - width: 2rem; - height: 2rem; - border-radius: 1rem; - } - } - .username { - font-size: 0.875rem; - color: ${palette.gray9}; - font-weight: bold; - a { - color: inherit; - text-decoration: none; - &:hover { - color: ${palette.gray8}; - } - } - } - margin-bottom: 1.5rem; - ${media.small} { - margin-bottom: 0.75rem; - } - } - .post-thumbnail { - margin-bottom: 1rem; - ${media.small} { - } - } - line-height: 1.5; - h2 { - font-size: 1.5rem; +function PostCard(props: PostCardProps) { + return ( + + + +

벨로그 v2 업데이트 안내

+

+ Node.js, PHP 등 익숙한 언어들을 던지고 생뚱맞은 장고를 택한 이유는 단 + 하나였습니다. 장고를 사용하시는 분들이 가장 많이 이야기하는, + 생산성입니다 내용이 더 길어지면 엉떻게 되니닌 +

+
+ 2020년 2월 10일 + · + 64개의 댓글 +
+
+
+
+ post-thumbnail + + by velopert + +
+
+ + 65 +
+
+
+ ); +} + +const Block = styled.div` + width: 20rem; + background: white; + border-radius: 4px; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.04); + margin: 1rem; + overflow: hidden; +`; + +const Content = styled.div` + padding: 1rem; + h4 { + font-size: 1rem; margin: 0; + margin-bottom: 0.25rem; + line-height: 1.5; + ${ellipsis} color: ${palette.gray9}; - word-break: keep-all; - ${media.small} { - font-size: 1rem; - } } p { - margin-bottom: 2rem; - margin-top: 0.5rem; - font-size: 1rem; - color: ${palette.gray7}; + margin: 0; word-break: keep-all; overflow-wrap: break-word; - ${media.small} { - font-size: 0.875rem; - margin-bottom: 1.5rem; - } + font-size: 0.875rem; + line-height: 1.5; + height: 3.9375rem; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + color: ${palette.gray7}; + margin-bottom: 1.5rem; } - .subinfo { - display: flex; - align-items: center; - margin-top: 1rem; + .sub-info { + font-size: 0.75rem; + line-height: 1.5; color: ${palette.gray6}; - font-size: 0.875rem; - ${media.small} { - font-size: 0.75rem; - } - span { - } .separator { - margin-left: 0.5rem; - margin-right: 0.5rem; - } - } - .tags-wrapper { - margin-bottom: -0.875rem; - ${media.small} { - margin-bottom: -0.5rem; + margin-left: 0.25rem; + margin-right: 0.25rem; } } - - & + & { - border-top: 1px solid ${palette.gray2}; - } `; -interface PostCardProps { - post: PartialPost; - hideUser?: boolean; -} - -const PostCard = ({ post, hideUser }: PostCardProps) => { - const prefetch = usePrefetchPost(post.user.username, post.url_slug); - const prefetchTimeoutId = useRef(null); - - const onMouseEnter = () => { - prefetchTimeoutId.current = setTimeout(prefetch, 2000); - }; - - const onMouseLeave = () => { - if (prefetchTimeoutId.current) { - clearTimeout(prefetchTimeoutId.current); - } - }; - - const url = `/@${post.user.username}/${post.url_slug}`; - const velogUrl = `/@${post.user.username}`; - - if (!post.user.profile) { - console.log(post); - } - return ( - - {!hideUser && ( -
- - thumbnail - -
- {post.user.username} -
-
- )} - {post.thumbnail && ( - - - - )} - -

{post.title}

- -

{post.short_description}

-
- {post.tags.map(tag => ( - - ))} -
-
- {formatDate(post.released_at)} -
·
- {post.comments_count}개의 댓글 - {post.is_private && ( - <> -
·
- - - - - )} -
-
- ); -}; - -export type PostCardSkeletonProps = { - hideUser?: boolean; -}; - -export function PostCardSkeleton({ hideUser }: PostCardSkeletonProps) { - return ( - - {!hideUser && ( -
- -
- -
-
- )} -
-
- -
-
-

- -

-
-
- -
-
- -
-
- -
-
-
- - - -
-
- - -
-
- ); -} - -const SkeletonBlock = styled(PostCardBlock)` - h2 { +const Footer = styled.div` + padding: 0.625rem 1rem; + border-top: 1px solid ${palette.gray0}; + display: flex; + font-size: 0.75rem; + line-height: 1.5; + justify-content: space-between; + .userinfo { display: flex; - margin-top: 1.375rem; - margin-bottom: 0.375rem; - } - .user-thumbnail-skeleton { - width: 3rem; - height: 3rem; - ${media.small} { - width: 2rem; - height: 2rem; - } - } - .thumbnail-skeleton-wrapper { - width: 100%; - padding-top: 52.35%; - position: relative; - .skeleton { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - } - .short-description { - margin-bottom: 2rem; - margin-top: 1rem; - font-size: 1rem; - .line { - display: flex; + align-items: center; + img { + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; + display: block; + margin-right: 0.5rem; } - .line + .line { - margin-top: 0.5rem; + span { + color: ${palette.gray6}; + b { + color: ${palette.gray8}; + } } } - .tags-skeleton { - line-height: 1; - font-size: 2rem; - ${media.small} { - font-size: 1.25rem; + .likes { + display: flex; + align-items: center; + svg { + width: 0.75rem; + height: 0.75rem; + margin-right: 0.5rem; } } `; -export default React.memo(PostCard); +export default PostCard; diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx new file mode 100644 index 00000000..9b5bbff5 --- /dev/null +++ b/src/components/common/PostCardGrid.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styled from 'styled-components'; +import PostCard from './PostCard'; + +export type PostCardGridProps = {}; + +function PostCardGrid(props: PostCardGridProps) { + return ( + + + + + + + + + + + ); +} + +const Block = styled.div` + display: flex; + margin: -1rem; + flex-wrap: wrap; +`; + +export default PostCardGrid; diff --git a/src/components/home/HomeHeader.tsx b/src/components/home/HomeHeader.tsx new file mode 100644 index 00000000..94bc4fd6 --- /dev/null +++ b/src/components/home/HomeHeader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Logo } from '../../static/svg'; +import RoundButton from '../common/RoundButton'; +import HomeResponsive from './HomeResponsive'; + +export type HomeHeaderProps = {}; + +function HomeHeader(props: HomeHeaderProps) { + return ( + + + + + {}}> + 로그인 + + + + + ); +} + +const Block = styled.div` + height: 4rem; +`; + +const Inner = styled(HomeResponsive)` + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; +`; + +const Right = styled.div` + display: flex; + align-items: center; +`; + +export default HomeHeader; diff --git a/src/components/home/HomeLayout.tsx b/src/components/home/HomeLayout.tsx new file mode 100644 index 00000000..f1bf4427 --- /dev/null +++ b/src/components/home/HomeLayout.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styled from 'styled-components'; + +export type HomeLayoutProps = { + main: React.ReactNode; + side: React.ReactNode; +}; + +function HomeLayout({ main, side }: HomeLayoutProps) { + return ( + +
{main}
+ {side} +
+ ); +} + +const Block = styled.div` + display: flex; + margin-top: 2rem; +`; +const Main = styled.main` + flex: 1; +`; +const Side = styled.aside` + margin-left: 6rem; + width: 16rem; +`; + +export default HomeLayout; diff --git a/src/components/home/HomeResponsive.tsx b/src/components/home/HomeResponsive.tsx new file mode 100644 index 00000000..eec78504 --- /dev/null +++ b/src/components/home/HomeResponsive.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styled from 'styled-components'; + +export type HomeResponsiveProps = { + className?: string; + children: React.ReactNode; +}; + +function HomeResponsive({ className, children }: HomeResponsiveProps) { + return {children}; +} + +const Block = styled.div` + width: 1728px; + margin-left: auto; + margin-right: auto; +`; + +export default HomeResponsive; diff --git a/src/components/home/HomeTab.tsx b/src/components/home/HomeTab.tsx new file mode 100644 index 00000000..78b04935 --- /dev/null +++ b/src/components/home/HomeTab.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import styled from 'styled-components'; +import { NavLink, useLocation } from 'react-router-dom'; +import palette from '../../lib/styles/palette'; +import { MdTrendingUp, MdAccessTime } from 'react-icons/md'; +import { useSpring, animated } from 'react-spring'; + +export type HomeTabProps = {}; + +function HomeTab(props: HomeTabProps) { + const location = useLocation(); + + const isRecent = location.pathname === '/recent'; + + const springStyle = useSpring({ + left: isRecent ? '50%' : '0%', + config: { + friction: 16, + tensiton: 160, + }, + }); + + return ( + + { + return ['/', '/trending'].indexOf(location.pathname) !== -1; + }} + > + + 트렌딩 + + + + 최신 + + + + ); +} + +const Block = styled.div` + margin-top: 1.5rem; + display: flex; + position: relative; + width: 14rem; + a { + width: 7rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.125rem; + text-decoration: none; + color: ${palette.gray6}; + height: 2.875rem; + svg { + font-size: 1.5rem; + margin-right: 0.5rem; + } + &.active { + color: ${palette.gray8}; + font-weight: bold; + } + } +`; + +const Indicator = styled(animated.div)` + width: 50%; + height: 2px; + position: absolute; + bottom: 0px; + background: ${palette.gray8}; +`; + +export default HomeTab; diff --git a/src/components/home/HomeTemplate.tsx b/src/components/home/HomeTemplate.tsx new file mode 100644 index 00000000..5ace3bdb --- /dev/null +++ b/src/components/home/HomeTemplate.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { Link } from 'react-router-dom'; + +const BackgroundStyle = createGlobalStyle` + body { + background: ${palette.gray0}; + } +`; + +export type HomeTemplateProps = { + children: React.ReactNode; +}; + +function HomeTemplate({ children }: HomeTemplateProps) { + return ( + <> + + {children} + + ); +} + +const Block = styled.div``; + +export default HomeTemplate; diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx index 57008c1e..26147bd4 100644 --- a/src/containers/main/RecentPosts.tsx +++ b/src/containers/main/RecentPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; diff --git a/src/containers/main/TrendingPosts.tsx b/src/containers/main/TrendingPosts.tsx index 5011d646..43d1567b 100644 --- a/src/containers/main/TrendingPosts.tsx +++ b/src/containers/main/TrendingPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_TRENDING_POSTS, GetTrendingPostsResponse, diff --git a/src/containers/search/SearchResult.tsx b/src/containers/search/SearchResult.tsx index 2386ffdb..eadc000b 100644 --- a/src/containers/search/SearchResult.tsx +++ b/src/containers/search/SearchResult.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import SearchResultInfo from '../../components/search/SearchResultInfo'; -import PostCardList from '../../components/common/PostCardList'; +import PostCardList from '../../components/common/FlatPostCardList'; import { useQuery } from '@apollo/react-hooks'; import { SearchPostsResponse, SEARCH_POSTS } from '../../lib/graphql/post'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; diff --git a/src/containers/tags/TagDetailContainer.tsx b/src/containers/tags/TagDetailContainer.tsx index 107dcdaf..4e7f2545 100644 --- a/src/containers/tags/TagDetailContainer.tsx +++ b/src/containers/tags/TagDetailContainer.tsx @@ -7,7 +7,7 @@ import { safe, ssrEnabled } from '../../lib/utils'; import useScrollPagination from '../../lib/hooks/useScrollPagination'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import useNotFound from '../../lib/hooks/useNotFound'; import { Helmet } from 'react-helmet-async'; diff --git a/src/containers/velog/UserPosts.tsx b/src/containers/velog/UserPosts.tsx index 74e9ad47..cc18a4e3 100644 --- a/src/containers/velog/UserPosts.tsx +++ b/src/containers/velog/UserPosts.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PostCardList, { PostCardListSkeleton, -} from '../../components/common/PostCardList'; +} from '../../components/common/FlatPostCardList'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; import PaginateWithScroll from '../../components/common/PaginateWithScroll'; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 9a9b8bd3..9b5b333a 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -54,6 +54,7 @@ export type PartialPost = { updated_at: string; tags: string[]; comments_count: number; + likes: number; }; // Generated by https://quicktype.io @@ -172,6 +173,7 @@ export const GET_TRENDING_POSTS = gql` title short_description thumbnail + likes user { id username diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 00000000..e8250447 --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import HomeTemplate from '../../components/home/HomeTemplate'; +import HomeHeader from '../../components/home/HomeHeader'; +import HomeTab from '../../components/home/HomeTab'; +import HomeResponsive from '../../components/home/HomeResponsive'; +import HomeLayout from '../../components/home/HomeLayout'; +import { Route } from 'react-router-dom'; +import TrendingPostsPage from './TrendingPostsPage'; +import RecentPostsPage from './RecentPostsPage'; + +export type HomePageProps = {}; + +function HomePage(props: HomePageProps) { + return ( + + + + + + + + + } + side={
Hello
} + /> +
+
+ ); +} + +export default HomePage; diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx new file mode 100644 index 00000000..8361183a --- /dev/null +++ b/src/pages/home/RecentPostsPage.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export type RecentPostsPageProps = {}; + +function RecentPostsPage(props: RecentPostsPageProps) { + return
몰라용
; +} + +export default RecentPostsPage; diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx new file mode 100644 index 00000000..65b5ebe6 --- /dev/null +++ b/src/pages/home/TrendingPostsPage.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import PostCardGrid from '../../components/common/PostCardGrid'; +import useTrendingPosts from './hooks/useTrendingPosts'; + +export type TrendingPageProps = {}; + +function TrendingPage(props: TrendingPageProps) { + const { data, loading } = useTrendingPosts(); + + return ; +} + +export default TrendingPage; diff --git a/src/pages/home/hooks/useTrendingPosts.ts b/src/pages/home/hooks/useTrendingPosts.ts new file mode 100644 index 00000000..563d976c --- /dev/null +++ b/src/pages/home/hooks/useTrendingPosts.ts @@ -0,0 +1,13 @@ +import { + GET_TRENDING_POSTS, + GetTrendingPostsResponse, +} from '../../../lib/graphql/post'; +import { useQuery } from '@apollo/react-hooks'; + +export default function useTrendingPosts() { + const { data, loading } = useQuery( + GET_TRENDING_POSTS, + ); + + return { data, loading }; +} From ecd41df5fb611692e631596225ce6505469c80c7 Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 27 Feb 2020 23:06:06 +0900 Subject: [PATCH 2/8] Implement post loading --- src/components/common/PostCard.tsx | 78 +++++++++++++++--------- src/components/common/PostCardGrid.tsx | 18 +++--- src/components/home/HomeSidebar.tsx | 24 ++++++++ src/lib/graphql/post.ts | 1 + src/pages/home/HomePage.tsx | 3 +- src/pages/home/RecentPostsPage.tsx | 7 ++- src/pages/home/TrendingPostsPage.tsx | 3 +- src/pages/home/hooks/useRecentPosts.ts | 37 +++++++++++ src/pages/home/hooks/useTrendingPosts.ts | 40 +++++++++++- 9 files changed, 169 insertions(+), 42 deletions(-) create mode 100644 src/components/home/HomeSidebar.tsx create mode 100644 src/pages/home/hooks/useRecentPosts.ts diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index 97ec8089..fd1d2f2f 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -1,46 +1,55 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import RatioImage from './RatioImage'; import { ellipsis } from '../../lib/styles/utils'; import palette from '../../lib/styles/palette'; import { LikeIcon } from '../../static/svg'; +import { PartialPost } from '../../lib/graphql/post'; +import { formatDate } from '../../lib/utils'; +import { userThumbnail } from '../../static/images'; +import optimizeImage from '../../lib/optimizeImage'; -export type PostCardProps = {}; +export type PostCardProps = { + post: PartialPost; +}; -function PostCard(props: PostCardProps) { +function PostCard({ post }: PostCardProps) { return ( - - -

벨로그 v2 업데이트 안내

-

- Node.js, PHP 등 익숙한 언어들을 던지고 생뚱맞은 장고를 택한 이유는 단 - 하나였습니다. 장고를 사용하시는 분들이 가장 많이 이야기하는, - 생산성입니다 내용이 더 길어지면 엉떻게 되니닌 -

+ {post.thumbnail && ( + + )} + +

{post.title}

+
+

+ {post.short_description} + {post.short_description.length === 150 && '...'} +

+
- 2020년 2월 10일 + {formatDate(post.released_at)} · - 64개의 댓글 + {post.comments_count}개의 댓글
post-thumbnail - by velopert + by {post.user.username}
- 65 + {post.likes}
@@ -54,10 +63,15 @@ const Block = styled.div` box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.04); margin: 1rem; overflow: hidden; + display: flex; + flex-direction: column; `; -const Content = styled.div` +const Content = styled.div<{ clamp: boolean }>` padding: 1rem; + display: flex; + flex: 1; + flex-direction: column; h4 { font-size: 1rem; margin: 0; @@ -66,18 +80,26 @@ const Content = styled.div` ${ellipsis} color: ${palette.gray9}; } + .description-wrapper { + flex: 1; + } p { margin: 0; word-break: keep-all; overflow-wrap: break-word; font-size: 0.875rem; line-height: 1.5; - height: 3.9375rem; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; + ${props => + props.clamp && + css` + height: 3.9375rem; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + `} + color: ${palette.gray7}; margin-bottom: 1.5rem; } diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx index 9b5bbff5..04fc6458 100644 --- a/src/components/common/PostCardGrid.tsx +++ b/src/components/common/PostCardGrid.tsx @@ -1,20 +1,18 @@ import React from 'react'; import styled from 'styled-components'; import PostCard from './PostCard'; +import { PartialPost } from '../../lib/graphql/post'; -export type PostCardGridProps = {}; +export type PostCardGridProps = { + posts: PartialPost[]; +}; -function PostCardGrid(props: PostCardGridProps) { +function PostCardGrid({ posts }: PostCardGridProps) { return ( - - - - - - - - + {posts.map(post => ( + + ))} ); } diff --git a/src/components/home/HomeSidebar.tsx b/src/components/home/HomeSidebar.tsx new file mode 100644 index 00000000..9b165d80 --- /dev/null +++ b/src/components/home/HomeSidebar.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import styled from 'styled-components'; +import MainNoticeWidgetContainer from '../../containers/main/MainNoticeWidgetContainer'; +import MainTagWidgetContainer from '../../containers/main/MainTagWidgetContainer'; +import MainRightFooter from '../main/MainRightFooter'; +import Sticky from '../common/Sticky'; + +export type HomeSidebarProps = {}; + +function HomeSidebar(props: HomeSidebarProps) { + return ( + + + + + + + + ); +} + +const Block = styled.div``; + +export default HomeSidebar; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 9b5b333a..ed2af1b5 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -162,6 +162,7 @@ export const GET_POST_LIST = gql` comments_count tags is_private + likes } } `; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index e8250447..eddc06e0 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -7,6 +7,7 @@ import HomeLayout from '../../components/home/HomeLayout'; import { Route } from 'react-router-dom'; import TrendingPostsPage from './TrendingPostsPage'; import RecentPostsPage from './RecentPostsPage'; +import HomeSidebar from '../../components/home/HomeSidebar'; export type HomePageProps = {}; @@ -27,7 +28,7 @@ function HomePage(props: HomePageProps) { } - side={
Hello
} + side={} /> diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx index 8361183a..c21d57ee 100644 --- a/src/pages/home/RecentPostsPage.tsx +++ b/src/pages/home/RecentPostsPage.tsx @@ -1,9 +1,14 @@ import React from 'react'; +import useRecentPosts from './hooks/useRecentPosts'; +import PostCardGrid from '../../components/common/PostCardGrid'; export type RecentPostsPageProps = {}; function RecentPostsPage(props: RecentPostsPageProps) { - return
몰라용
; + const { data, loading } = useRecentPosts(); + + if (!data) return null; + return ; } export default RecentPostsPage; diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx index 65b5ebe6..ac0e4702 100644 --- a/src/pages/home/TrendingPostsPage.tsx +++ b/src/pages/home/TrendingPostsPage.tsx @@ -7,7 +7,8 @@ export type TrendingPageProps = {}; function TrendingPage(props: TrendingPageProps) { const { data, loading } = useTrendingPosts(); - return ; + if (!data) return null; + return ; } export default TrendingPage; diff --git a/src/pages/home/hooks/useRecentPosts.ts b/src/pages/home/hooks/useRecentPosts.ts new file mode 100644 index 00000000..0e898c31 --- /dev/null +++ b/src/pages/home/hooks/useRecentPosts.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@apollo/react-hooks'; +import { GET_POST_LIST, PartialPost } from '../../../lib/graphql/post'; +import { useCallback } from 'react'; +import useScrollPagination from '../../../lib/hooks/useScrollPagination'; + +export default function useRecentPosts() { + const { data, loading, fetchMore } = useQuery<{ posts: PartialPost[] }>( + GET_POST_LIST, + {}, + ); + + const onLoadMore = useCallback( + (cursor: string) => { + fetchMore({ + variables: { + cursor, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return { + posts: [...prev.posts, ...fetchMoreResult.posts], + }; + }, + }); + }, + [fetchMore], + ); + + const cursor = data?.posts[data?.posts.length - 1]?.id; + + useScrollPagination({ + cursor, + onLoadMore, + }); + + return { data, loading }; +} diff --git a/src/pages/home/hooks/useTrendingPosts.ts b/src/pages/home/hooks/useTrendingPosts.ts index 563d976c..afa21055 100644 --- a/src/pages/home/hooks/useTrendingPosts.ts +++ b/src/pages/home/hooks/useTrendingPosts.ts @@ -3,11 +3,49 @@ import { GetTrendingPostsResponse, } from '../../../lib/graphql/post'; import { useQuery } from '@apollo/react-hooks'; +import { useCallback } from 'react'; +import useScrollPagination from '../../../lib/hooks/useScrollPagination'; export default function useTrendingPosts() { - const { data, loading } = useQuery( + const { data, loading, fetchMore } = useQuery( GET_TRENDING_POSTS, ); + const onLoadMoreByOffset = useCallback( + (offset: number) => { + fetchMore({ + variables: { + offset, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + + // filter unique posts + const idMap: Record = prev.trendingPosts.reduce( + (acc, current) => { + Object.assign(acc, { [current.id]: true }); + return acc; + }, + {}, + ); + + const uniquePosts = fetchMoreResult.trendingPosts.filter( + post => !idMap[post.id], + ); + + return { + trendingPosts: [...prev.trendingPosts, ...uniquePosts], + }; + }, + }); + }, + [fetchMore], + ); + + useScrollPagination({ + offset: data?.trendingPosts.length, + onLoadMoreByOffset, + }); + return { data, loading }; } From c63f99a25ba48042bd8b0beab86d046a3959b9c5 Mon Sep 17 00:00:00 2001 From: velopert Date: Sun, 1 Mar 2020 17:52:01 +0900 Subject: [PATCH 3/8] Implement responsive design to grid --- package.json | 2 + src/components/base/Header.tsx | 109 +++++++-------- src/components/common/PostCard.tsx | 147 ++++++++++++++++++--- src/components/common/PostCardGrid.tsx | 16 ++- src/components/home/FloatingHomeHeader.tsx | 93 +++++++++++++ src/components/home/HomeHeader.tsx | 30 ++++- src/components/home/HomeLayout.tsx | 8 ++ src/components/home/HomeResponsive.tsx | 16 +++ src/components/home/HomeSidebar.tsx | 2 +- src/components/home/hooks/useHeader.ts | 25 ++++ src/components/main/MainRightFooter.tsx | 4 + src/containers/main/TrendingPosts.tsx | 2 +- src/lib/graphql/post.ts | 2 + src/lib/hooks/useScrollPagination.ts | 23 +++- src/pages/home/HomePage.tsx | 2 + src/pages/home/RecentPostsPage.tsx | 13 +- src/pages/home/TrendingPostsPage.tsx | 16 ++- src/pages/home/hooks/useRecentPosts.ts | 17 ++- src/pages/home/hooks/useTrendingPosts.ts | 16 ++- yarn.lock | 34 ++++- 20 files changed, 479 insertions(+), 98 deletions(-) create mode 100644 src/components/home/FloatingHomeHeader.tsx create mode 100644 src/components/home/hooks/useHeader.ts diff --git a/package.json b/package.json index 1b7ea214..fc4d1ea5 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react-router-dom": "^5.1.3", "@types/react-textarea-autosize": "^4.3.5", "@types/react-toastify": "^4.1.0", + "@types/react-virtualized": "^9.21.8", "@types/sanitize-html": "^1.20.2", "@types/styled-components": "^4.4.1", "@types/throttle-debounce": "^2.1.0", @@ -120,6 +121,7 @@ "react-textarea-autosize": "^7.1.2", "react-toastify": "^5.5.0", "react-use": "^13.12.2", + "react-virtualized": "^9.21.2", "redux": "^4.0.4", "redux-devtools-extension": "^2.13.8", "remark": "^11.0.2", diff --git a/src/components/base/Header.tsx b/src/components/base/Header.tsx index 336c281d..7ee2766e 100644 --- a/src/components/base/Header.tsx +++ b/src/components/base/Header.tsx @@ -12,15 +12,16 @@ import HeaderLogo from './HeaderLogo'; import media from '../../lib/styles/media'; import { SearchIcon2 } from '../../static/svg'; import { Link } from 'react-router-dom'; +import HomeResponsive from '../home/HomeResponsive'; const HeaderBlock = styled.div<{ floating: boolean }>` width: 100%; - > .wrapper { - width: 1200px; + .wrapper { + /* width: 1200px; */ + width: 100%; height: 4rem; - margin: 0 auto; - padding-left: 1rem; - padding-right: 1rem; + /* padding-left: 1rem; + padding-right: 1rem; */ display: flex; justify-content: space-between; align-items: center; @@ -36,10 +37,10 @@ const HeaderBlock = styled.div<{ floating: boolean }>` } ${media.large} { - width: 1024px; + /* width: 1024px; */ } ${media.medium} { - width: 100%; + /* width: 100%; */ .write-button { display: none; } @@ -122,63 +123,65 @@ const Header: React.FC = ({ style={{ marginTop: floating ? floatingMargin : 0 }} data-testid="Header" > -
-
- -
-
- {/* {velogUsername ? ( + +
+
+ +
+
+ {/* {velogUsername ? ( ) : ( )} */} - {!isSearch && ( - - - - )} - {user ? ( -
+ {!isSearch && ( + + + + )} + {user ? ( +
+ + 새 글 작성 + + + +
+ ) : ( - 새 글 작성 + 로그인 - - -
- ) : ( - - 로그인 - - )} + )} +
-
+ {floating && } diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index fd1d2f2f..e12e4b56 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -8,29 +8,39 @@ import { PartialPost } from '../../lib/graphql/post'; import { formatDate } from '../../lib/utils'; import { userThumbnail } from '../../static/images'; import optimizeImage from '../../lib/optimizeImage'; +import SkeletonTexts from './SkeletonTexts'; +import Skeleton from './Skeleton'; +import { mediaQuery } from '../../lib/styles/media'; +import { Link } from 'react-router-dom'; export type PostCardProps = { post: PartialPost; }; function PostCard({ post }: PostCardProps) { + const url = `/@${post.user.username}/${post.url_slug}`; + return ( {post.thumbnail && ( - + + + )} -

{post.title}

-
-

- {post.short_description} - {post.short_description.length === 150 && '...'} -

-
+ +

{post.title}

+
+

+ {post.short_description.replace(/:/g, ':')} + {post.short_description.length === 150 && '...'} +

+
+
{formatDate(post.released_at)} · @@ -38,7 +48,7 @@ function PostCard({ post }: PostCardProps) {