-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
);
}
-const SkeletonBlock = styled(PostCardBlock)`
- h2 {
+const StyledLink = styled(Link)`
+ display: block;
+ color: inherit;
+ text-decoration: none;
+`;
+
+const Block = styled.div`
+ width: 20rem;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.04);
+ transition: 0.25s box-shadow ease-in, 0.25s transform ease-in;
+ &:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 12px 20px 0 rgba(0, 0, 0, 0.08);
+ ${mediaQuery(1024)} {
+ transform: none;
+ }
+ }
+ margin: 1rem;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ ${mediaQuery(944)} {
+ width: calc(50% - 2rem);
+ }
+ ${mediaQuery(767)} {
+ margin: 0;
+ width: 100%;
+ & + & {
+ margin-top: 1rem;
+ }
+ }
+`;
+
+const Content = styled.div<{ clamp: boolean }>`
+ padding: 1rem;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ h4 {
+ font-size: 1rem;
+ margin: 0;
+ margin-bottom: 0.25rem;
+ line-height: 1.5;
+ ${ellipsis}
+ color: ${palette.gray9};
+ ${mediaQuery(767)} {
+ white-space: initial;
+ }
+ }
+ .description-wrapper {
+ flex: 1;
+ }
+ p {
+ margin: 0;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ ${props =>
+ props.clamp &&
+ css`
+ height: 3.9375rem;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ `}
+ /* ${props =>
+ !props.clamp &&
+ css`
+ height: 15.875rem;
+ `} */
+
+ color: ${palette.gray7};
+ margin-bottom: 1.5rem;
+ }
+ .sub-info {
+ font-size: 0.75rem;
+ line-height: 1.5;
+ color: ${palette.gray6};
+ .separator {
+ margin-left: 0.25rem;
+ margin-right: 0.25rem;
+ }
+ }
+`;
+
+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 {
+ text-decoration: none;
+ color: inherit;
display: flex;
- margin-top: 1.375rem;
- margin-bottom: 0.375rem;
+ align-items: center;
+ img {
+ object-fit: cover;
+ border-radius: 50%;
+ width: 1.5rem;
+ height: 1.5rem;
+ display: block;
+ margin-right: 0.5rem;
+ }
+ span {
+ color: ${palette.gray6};
+ b {
+ color: ${palette.gray8};
+ }
+ }
}
- .user-thumbnail-skeleton {
- width: 3rem;
- height: 3rem;
- ${media.small} {
- width: 2rem;
- height: 2rem;
+ .likes {
+ display: flex;
+ align-items: center;
+ svg {
+ width: 0.75rem;
+ height: 0.75rem;
+ margin-right: 0.5rem;
}
}
- .thumbnail-skeleton-wrapper {
+`;
+
+const SkeletonBlock = styled(Block)`
+ .skeleton-thumbnail-wrapper {
width: 100%;
- padding-top: 52.35%;
+ padding-top: 52.19%;
position: relative;
- .skeleton {
+ .skeleton-thumbnail {
position: absolute;
top: 0;
left: 0;
@@ -276,22 +271,15 @@ const SkeletonBlock = styled(PostCardBlock)`
height: 100%;
}
}
- .short-description {
- margin-bottom: 2rem;
- margin-top: 1rem;
- font-size: 1rem;
+ .lines {
+ height: 3.9375rem;
+ font-size: 0.875rem;
+ margin-bottom: 1.5rem;
.line {
display: flex;
}
.line + .line {
- margin-top: 0.5rem;
- }
- }
- .tags-skeleton {
- line-height: 1;
- font-size: 2rem;
- ${media.small} {
- font-size: 1.25rem;
+ margin-top: 0.3rem;
}
}
`;
diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx
new file mode 100644
index 00000000..84416512
--- /dev/null
+++ b/src/components/common/PostCardGrid.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import styled from 'styled-components';
+import PostCard, { PostCardSkeleton } from './PostCard';
+import { PartialPost } from '../../lib/graphql/post';
+import { mediaQuery } from '../../lib/styles/media';
+
+export type PostCardGridProps = {
+ posts: PartialPost[];
+};
+
+function PostCardGrid({ posts }: PostCardGridProps) {
+ return (
+
+ {posts.map(post => (
+
+ ))}
+
+ );
+}
+
+export function PostCardGridSkeleton() {
+ return (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+const Block = styled.div`
+ display: flex;
+ margin: -1rem;
+ flex-wrap: wrap;
+ ${mediaQuery(767)} {
+ margin: 0;
+ }
+`;
+
+export default PostCardGrid;
diff --git a/src/components/common/RoundButton.tsx b/src/components/common/RoundButton.tsx
index 4b694b61..eb17f6be 100644
--- a/src/components/common/RoundButton.tsx
+++ b/src/components/common/RoundButton.tsx
@@ -63,7 +63,7 @@ const RoundButtonBlock = styled.button
`
${props =>
props.border &&
css`
- background: transparent;
+ background: white;
border: 1px solid ${props => buttonColorMap[props.color].background};
color: ${props => buttonColorMap[props.color].background};
&:hover {
diff --git a/src/components/home/FloatingHomeHeader.tsx b/src/components/home/FloatingHomeHeader.tsx
new file mode 100644
index 00000000..cd8741a0
--- /dev/null
+++ b/src/components/home/FloatingHomeHeader.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useCallback, useRef, useState } from 'react';
+import styled from 'styled-components';
+import HomeHeader from './HomeHeader';
+import HomeTab from './HomeTab';
+import HomeResponsive from './HomeResponsive';
+import { getScrollTop } from '../../lib/utils';
+
+export type FloatingHomeHeaderProps = {};
+
+function FloatingHomeHeader(props: FloatingHomeHeaderProps) {
+ const [visible, setVisible] = useState(false);
+ const [marginTop, setMarginTop] = useState(-102);
+
+ const prevScrollTop = useRef(0);
+ const direction = useRef<'UP' | 'DOWN'>('DOWN');
+ const transitionPoint = useRef(0);
+
+ const onScroll = useCallback(() => {
+ const scrollTop = getScrollTop();
+ const nextDirection = prevScrollTop.current > scrollTop ? 'UP' : 'DOWN';
+
+ if (
+ direction.current === 'DOWN' &&
+ nextDirection === 'UP' &&
+ transitionPoint.current - scrollTop < 0
+ ) {
+ setVisible(true);
+ transitionPoint.current = scrollTop;
+ }
+
+ if (
+ direction.current === 'UP' &&
+ nextDirection === 'DOWN' &&
+ scrollTop - transitionPoint.current < -102
+ ) {
+ transitionPoint.current = scrollTop + 102;
+ }
+
+ if (scrollTop < 64) {
+ setVisible(false);
+ }
+
+ setMarginTop(Math.min(0, -102 + transitionPoint.current - scrollTop));
+
+ direction.current = nextDirection;
+ prevScrollTop.current = scrollTop;
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('scroll', onScroll);
+ return () => {
+ document.removeEventListener('scroll', onScroll);
+ };
+ }, [onScroll]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const Block = styled.div`
+ position: fixed;
+ top: 0;
+ background: white;
+ width: 100%;
+ z-index: 10;
+
+ box-shadow: 0px 0 8px rgba(0, 0, 0, 0.08);
+ .tab-wrapper {
+ margin-top: -2rem;
+ }
+`;
+
+export default FloatingHomeHeader;
diff --git a/src/components/home/HomeHeader.tsx b/src/components/home/HomeHeader.tsx
new file mode 100644
index 00000000..1a10527d
--- /dev/null
+++ b/src/components/home/HomeHeader.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import styled from 'styled-components';
+import { Logo, SearchIcon2 } from '../../static/svg';
+import RoundButton from '../common/RoundButton';
+import HomeResponsive from './HomeResponsive';
+import useHeader from './hooks/useHeader';
+import HeaderUserIcon from '../base/HeaderUserIcon';
+import useToggle from '../../lib/hooks/useToggle';
+import HeaderUserMenu from '../base/HeaderUserMenu';
+import { Link } from 'react-router-dom';
+import { mediaQuery } from '../../lib/styles/media';
+
+export type HomeHeaderProps = {};
+
+function HomeHeader(props: HomeHeaderProps) {
+ const { user, onLoginClick, onLogout } = useHeader();
+ const [userMenu, toggleUserMenu] = useToggle(false);
+
+ return (
+
+
+
+ {user ? (
+
+
+
+
+
+ 새 글 작성
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ 로그인
+
+
+ )}
+
+
+ );
+}
+
+const Block = styled.div`
+ height: 4rem;
+`;
+
+const SearchButton = styled(Link)`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ width: 2.5rem;
+ height: 2.5rem;
+ outline: none;
+ border-radius: 50%;
+ cursor: pointer;
+ &:hover {
+ background: rgba(0, 0, 0, 0.045);
+ }
+ svg {
+ width: 1.125rem;
+ height: 1.125rem;
+ }
+ margin-right: 0.75rem;
+`;
+
+const Inner = styled(HomeResponsive)`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+`;
+
+const Right = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ .write-button {
+ ${mediaQuery(376)} {
+ display: none;
+ }
+ }
+`;
+
+export default HomeHeader;
diff --git a/src/components/home/HomeLayout.tsx b/src/components/home/HomeLayout.tsx
new file mode 100644
index 00000000..8bc03961
--- /dev/null
+++ b/src/components/home/HomeLayout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import styled from 'styled-components';
+import { mediaQuery } from '../../lib/styles/media';
+
+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;
+ ${mediaQuery(1440)} {
+ margin-left: 3rem;
+ width: 12rem;
+ }
+ ${mediaQuery(944)} {
+ display: none;
+ }
+`;
+
+export default HomeLayout;
diff --git a/src/components/home/HomeResponsive.tsx b/src/components/home/HomeResponsive.tsx
new file mode 100644
index 00000000..fff46d05
--- /dev/null
+++ b/src/components/home/HomeResponsive.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import styled from 'styled-components';
+import { mediaQuery } from '../../lib/styles/media';
+
+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;
+ ${mediaQuery(1919)} {
+ width: 1376px;
+ }
+ ${mediaQuery(1440)} {
+ width: 1280px;
+ }
+ ${mediaQuery(1312)} {
+ width: 912px;
+ }
+ ${mediaQuery(944)} {
+ width: calc(100% - 2rem);
+ }
+ ${mediaQuery(767)} {
+ width: calc(100% - 2rem);
+ }
+`;
+
+export default HomeResponsive;
diff --git a/src/components/home/HomeSidebar.tsx b/src/components/home/HomeSidebar.tsx
new file mode 100644
index 00000000..9495daac
--- /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/components/home/HomeTab.tsx b/src/components/home/HomeTab.tsx
new file mode 100644
index 00000000..86498a62
--- /dev/null
+++ b/src/components/home/HomeTab.tsx
@@ -0,0 +1,132 @@
+import React, { useRef } from 'react';
+import styled from 'styled-components';
+import { NavLink, useLocation } from 'react-router-dom';
+import palette from '../../lib/styles/palette';
+import { MdTrendingUp, MdAccessTime, MdMoreVert } from 'react-icons/md';
+import { useSpring, animated } from 'react-spring';
+import { mediaQuery } from '../../lib/styles/media';
+import useToggle from '../../lib/hooks/useToggle';
+import MainMobileHeadExtra from '../../components/main/MainMobileHeadExtra';
+
+export type HomeTabProps = {};
+
+function HomeTab(props: HomeTabProps) {
+ const location = useLocation();
+
+ const isRecent = location.pathname === '/recent';
+ const [extra, toggle] = useToggle(false);
+ const moreButtonRef = useRef(null);
+
+ const onClose = (e: React.MouseEvent) => {
+ if (!moreButtonRef.current) return;
+ if (
+ e.target === moreButtonRef.current ||
+ moreButtonRef.current.contains(e.target as Node)
+ ) {
+ return;
+ }
+ toggle();
+ };
+
+ const springStyle = useSpring({
+ left: isRecent ? '50%' : '0%',
+ config: {
+ friction: 16,
+ tensiton: 160,
+ },
+ });
+
+ return (
+
+
+ {
+ return ['/', '/trending'].indexOf(location.pathname) !== -1;
+ }}
+ >
+
+ 트렌딩
+
+
+
+ 최신
+
+
+
+
+
+
+
+
+ );
+}
+
+const Wrapper = styled.div`
+ margin-top: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ .more {
+ cursor: pointer;
+ font-size: 1.5rem;
+ color: ${palette.gray6};
+ }
+`;
+
+const MobileMore = styled.div`
+ display: none;
+ ${mediaQuery(944)} {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+`;
+
+const Block = styled.div`
+ display: flex;
+ position: relative;
+ width: 14rem;
+ ${mediaQuery(944)} {
+ width: 10rem;
+ }
+ 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;
+ }
+
+ ${mediaQuery(944)} {
+ font-size: 1rem;
+ width: 5rem;
+ svg {
+ font-size: 1.25rem;
+ }
+ }
+ }
+`;
+
+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/components/home/hooks/useHeader.ts b/src/components/home/hooks/useHeader.ts
new file mode 100644
index 00000000..27cce473
--- /dev/null
+++ b/src/components/home/hooks/useHeader.ts
@@ -0,0 +1,25 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { useCallback } from 'react';
+import { showAuthModal } from '../../../modules/core';
+import { RootState } from '../../../modules';
+import { logout } from '../../../lib/api/auth';
+import storage from '../../../lib/storage';
+
+export default function useHeader() {
+ const dispatch = useDispatch();
+ const user = useSelector((state: RootState) => state.core.user);
+
+ const onLoginClick = useCallback(() => {
+ dispatch(showAuthModal('LOGIN'));
+ }, [dispatch]);
+
+ const onLogout = useCallback(async () => {
+ try {
+ await logout();
+ } catch {}
+ storage.removeItem('CURRENT_USER');
+ window.location.href = '/';
+ }, []);
+
+ return { user, onLoginClick, onLogout };
+}
diff --git a/src/components/main/MainRightFooter.tsx b/src/components/main/MainRightFooter.tsx
index d0ccc00f..2652f138 100644
--- a/src/components/main/MainRightFooter.tsx
+++ b/src/components/main/MainRightFooter.tsx
@@ -2,9 +2,13 @@ import * as React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
+import media from '../../lib/styles/media';
const MainRightFooterBlock = styled.div`
margin-top: 6.25rem;
+ ${media.medium} {
+ margin-top: 3rem;
+ }
line-height: 1.5;
font-size: 0.875rem;
.links {
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..a2220d69 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,
@@ -66,7 +66,7 @@ const TrendingPosts: React.FC = props => {
- {loading && }
+
>
);
};
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..03425d77 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
@@ -136,12 +137,14 @@ export const GET_POST_LIST = gql`
$username: String
$temp_only: Boolean
$tag: String
+ $limit: Int
) {
posts(
cursor: $cursor
username: $username
temp_only: $temp_only
tag: $tag
+ limit: $limit
) {
id
title
@@ -161,6 +164,7 @@ export const GET_POST_LIST = gql`
comments_count
tags
is_private
+ likes
}
}
`;
@@ -172,6 +176,7 @@ export const GET_TRENDING_POSTS = gql`
title
short_description
thumbnail
+ likes
user {
id
username
diff --git a/src/lib/hooks/useScrollPagination.ts b/src/lib/hooks/useScrollPagination.ts
index e353a9c0..bb7d12d8 100644
--- a/src/lib/hooks/useScrollPagination.ts
+++ b/src/lib/hooks/useScrollPagination.ts
@@ -1,5 +1,5 @@
import { useRef, useCallback, useEffect } from 'react';
-import { getScrollBottom } from '../utils';
+import { getScrollBottom, getScrollTop } from '../utils';
type Params = {
offset?: number | null;
@@ -18,19 +18,28 @@ export default function useScrollPagination({
}: Params) {
const last = useRef(null);
- const loadMore = useCallback(() => {
+ const preventBottomStick = useCallback(() => {
+ console.log(getScrollBottom());
+ if (getScrollBottom() === 0) {
+ window.scrollTo(0, getScrollTop() - 1);
+ }
+ }, []);
+
+ const loadMore = useCallback(async () => {
if (!cursor || !onLoadMore) return;
if (cursor === last.current) return;
- onLoadMore(cursor);
last.current = cursor;
- }, [cursor, onLoadMore]);
+ await onLoadMore(cursor);
+ preventBottomStick();
+ }, [cursor, onLoadMore, preventBottomStick]);
- const loadMoreUsingOffset = useCallback(() => {
+ const loadMoreUsingOffset = useCallback(async () => {
if (stop || !offset || !onLoadMoreByOffset) return;
if (offset === last.current) return;
- onLoadMoreByOffset(offset);
last.current = offset;
- }, [offset, onLoadMoreByOffset, stop]);
+ await onLoadMoreByOffset(offset);
+ preventBottomStick();
+ }, [offset, onLoadMoreByOffset, preventBottomStick, stop]);
const onScroll = useCallback(() => {
const scrollBottom = getScrollBottom();
diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx
new file mode 100644
index 00000000..cd0cbdd4
--- /dev/null
+++ b/src/pages/home/HomePage.tsx
@@ -0,0 +1,40 @@
+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';
+import HomeSidebar from '../../components/home/HomeSidebar';
+import FloatingHomeHeader from '../../components/home/FloatingHomeHeader';
+
+export type HomePageProps = {};
+
+function HomePage(props: HomePageProps) {
+ return (
+
+
+
+
+
+
+
+
+ >
+ }
+ side={}
+ />
+
+
+ );
+}
+
+export default HomePage;
diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx
new file mode 100644
index 00000000..a846077d
--- /dev/null
+++ b/src/pages/home/RecentPostsPage.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import useRecentPosts from './hooks/useRecentPosts';
+import PostCardGrid, {
+ PostCardGridSkeleton,
+} from '../../components/common/PostCardGrid';
+
+export type RecentPostsPageProps = {};
+
+function RecentPostsPage(props: RecentPostsPageProps) {
+ const { data, loading } = useRecentPosts();
+
+ if (!data) return ;
+ return (
+ <>
+
+ {loading && }
+ >
+ );
+}
+
+export default RecentPostsPage;
diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx
new file mode 100644
index 00000000..a967dbd2
--- /dev/null
+++ b/src/pages/home/TrendingPostsPage.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import PostCardGrid, {
+ PostCardGridSkeleton,
+} from '../../components/common/PostCardGrid';
+import useTrendingPosts from './hooks/useTrendingPosts';
+
+export type TrendingPageProps = {};
+
+function TrendingPage(props: TrendingPageProps) {
+ const { data, loading, isFinished } = useTrendingPosts();
+
+ console.log(loading);
+ if (!data) return ;
+ return (
+ <>
+
+ {data && loading && }
+ >
+ );
+}
+
+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..de990a0d
--- /dev/null
+++ b/src/pages/home/hooks/useRecentPosts.ts
@@ -0,0 +1,48 @@
+import { useQuery } from '@apollo/react-hooks';
+import { GET_POST_LIST, PartialPost } from '../../../lib/graphql/post';
+import { useCallback, useState } from 'react';
+import useScrollPagination from '../../../lib/hooks/useScrollPagination';
+
+export default function useRecentPosts() {
+ const { data, loading, fetchMore } = useQuery<{ posts: PartialPost[] }>(
+ GET_POST_LIST,
+ {
+ variables: {
+ limit: 24,
+ },
+ // https://github.com/apollographql/apollo-client/issues/1617
+ notifyOnNetworkStatusChange: true,
+ },
+ );
+ const [isFinished, setIsFinished] = useState(false);
+
+ const onLoadMore = useCallback(
+ (cursor: string) => {
+ fetchMore({
+ variables: {
+ cursor,
+ limit: 24,
+ },
+ updateQuery: (prev, { fetchMoreResult }) => {
+ if (!fetchMoreResult) return prev;
+ if (fetchMoreResult.posts.length === 0) {
+ setIsFinished(true);
+ }
+ return {
+ posts: [...prev.posts, ...fetchMoreResult.posts],
+ };
+ },
+ });
+ },
+ [fetchMore],
+ );
+
+ const cursor = data?.posts[data?.posts.length - 1]?.id;
+
+ useScrollPagination({
+ cursor,
+ onLoadMore,
+ });
+
+ return { data, loading, isFinished };
+}
diff --git a/src/pages/home/hooks/useTrendingPosts.ts b/src/pages/home/hooks/useTrendingPosts.ts
new file mode 100644
index 00000000..6e4cefce
--- /dev/null
+++ b/src/pages/home/hooks/useTrendingPosts.ts
@@ -0,0 +1,63 @@
+import {
+ GET_TRENDING_POSTS,
+ GetTrendingPostsResponse,
+} from '../../../lib/graphql/post';
+import { useQuery } from '@apollo/react-hooks';
+import { useCallback, useState } from 'react';
+import useScrollPagination from '../../../lib/hooks/useScrollPagination';
+
+export default function useTrendingPosts() {
+ const { data, loading, fetchMore } = useQuery(
+ GET_TRENDING_POSTS,
+ {
+ variables: {
+ limit: 24,
+ },
+ // https://github.com/apollographql/apollo-client/issues/1617
+ notifyOnNetworkStatusChange: true,
+ },
+ );
+ const [isFinished, setIsFinished] = useState(false);
+
+ const onLoadMoreByOffset = useCallback(
+ (offset: number) => {
+ fetchMore({
+ variables: {
+ offset,
+ limit: 24,
+ },
+ updateQuery: (prev, { fetchMoreResult }) => {
+ if (!fetchMoreResult) return prev;
+ if (fetchMoreResult.trendingPosts.length === 0) {
+ setIsFinished(true);
+ }
+
+ // 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, isFinished };
+}
diff --git a/yarn.lock b/yarn.lock
index 95b3f3d7..bc1a77f1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1842,6 +1842,14 @@
dependencies:
react-toastify "*"
+"@types/react-virtualized@^9.21.8":
+ version "9.21.8"
+ resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.8.tgz#dc0150a75fd6e42f33729886463ece04d03367ea"
+ integrity sha512-7fZoA0Azd2jLIE9XC37fMZgMqaJe3o3pfzGjvrzphoKjBCdT4oNl6wikvo4dDMESDnpkZ8DvVTc7aSe4DW86Ew==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/react" "*"
+
"@types/react@*", "@types/react@^16.9.0":
version "16.9.16"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.16.tgz#4f12515707148b1f53a8eaa4341dae5dfefb066d"
@@ -3569,6 +3577,11 @@ clone-response@1.0.2:
dependencies:
mimic-response "^1.0.0"
+clsx@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
+ integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
+
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@@ -4672,7 +4685,7 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
-dom-helpers@^5.0.1:
+dom-helpers@^5.0.0, dom-helpers@^5.0.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821"
integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw==
@@ -8126,7 +8139,7 @@ longest-streak@^2.0.1:
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.3.tgz#3de7a3f47ee18e9074ded8575b5c091f5d0a4105"
integrity sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw==
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -10641,6 +10654,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
+react-lifecycles-compat@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+ integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
react-outside-click-handler@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115"
@@ -10750,6 +10768,18 @@ react-use@^13.12.2:
ts-easing "^0.2.0"
tslib "^1.10.0"
+react-virtualized@^9.21.2:
+ version "9.21.2"
+ resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e"
+ integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==
+ dependencies:
+ babel-runtime "^6.26.0"
+ clsx "^1.0.1"
+ dom-helpers "^5.0.0"
+ loose-envify "^1.3.0"
+ prop-types "^15.6.0"
+ react-lifecycles-compat "^3.0.4"
+
react@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83"