diff --git a/src/App.tsx b/src/App.tsx index c20e05af..515f66f6 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; -// import MainPage from './pages/main/MainPage'; +import { Route, Switch, Redirect } from 'react-router-dom'; // import PostPage from './pages/PostPage'; import loadable from '@loadable/component'; @@ -13,6 +12,7 @@ import ErrorBoundary from './components/error/ErrorBoundary'; import NotFoundPage from './pages/NotFoundPage'; import { Helmet } from 'react-helmet-async'; import HomePage from './pages/home/HomePage'; +import MainPageTemplate from './components/main/MainPageTemplate'; const loadableConfig = { fallback: , @@ -39,6 +39,12 @@ const SettingPage = loadable( loadableConfig, ); const SuccessPage = loadable(() => import('./pages/SuccessPage')); +const ReadingListPage = loadable( + () => import('./pages/readingList/ReadingListPage'), + { + fallback: , + }, +); interface AppProps {} @@ -73,6 +79,8 @@ const App: React.FC = props => { + + } /> diff --git a/src/components/base/Header.tsx b/src/components/base/Header.tsx index 7ee2766e..24f64a59 100644 --- a/src/components/base/Header.tsx +++ b/src/components/base/Header.tsx @@ -12,7 +12,7 @@ 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'; +import MainResponsive from '../main/MainResponsive'; const HeaderBlock = styled.div<{ floating: boolean }>` width: 100%; @@ -123,7 +123,7 @@ const Header: React.FC = ({ style={{ marginTop: floating ? floatingMargin : 0 }} data-testid="Header" > - +
= ({ )}
-
+ {floating && } diff --git a/src/components/base/HeaderUserMenu.tsx b/src/components/base/HeaderUserMenu.tsx index e2aa6a0a..ad7c80e9 100644 --- a/src/components/base/HeaderUserMenu.tsx +++ b/src/components/base/HeaderUserMenu.tsx @@ -39,6 +39,7 @@ const HeaderUserMenu: React.FC = ({ 내 벨로그 임시 글 + 읽기 목록 설정 로그아웃 diff --git a/src/components/base/HeaderUserMenuItem.tsx b/src/components/base/HeaderUserMenuItem.tsx index a6111997..ef8da664 100644 --- a/src/components/base/HeaderUserMenuItem.tsx +++ b/src/components/base/HeaderUserMenuItem.tsx @@ -14,6 +14,7 @@ const HeaderUserMenuItemBlock = styled.div` padding: 0.75rem 1rem; line-height: 1.5; font-weight: 500; + cursor: pointer; &:hover { background: ${palette.gray0}; } diff --git a/src/components/common/HorizontalTab.tsx b/src/components/common/HorizontalTab.tsx index 131b2c5b..895a8d19 100644 --- a/src/components/common/HorizontalTab.tsx +++ b/src/components/common/HorizontalTab.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; import palette from '../../lib/styles/palette'; import { useSpring, animated } from 'react-spring'; @@ -10,6 +10,8 @@ export type HorizontalTabProps = { children: React.ReactElement[]; activeTab: string; tabWidth: number; + align: 'center' | 'left'; + theme: 'teal' | 'gray'; }; function HorizontalTab({ @@ -17,6 +19,8 @@ function HorizontalTab({ children, activeTab, tabWidth, + align, + theme, }: HorizontalTabProps) { const activeIndex = React.Children.toArray(children).findIndex( tab => tab.props.name === activeTab, @@ -34,15 +38,16 @@ function HorizontalTab({ }); return ( - +
{React.Children.map(children, tab => { return React.cloneElement(tab, { active: tab.props.name === activeTab, width: `${tabWidth}rem`, + theme, }); })} - +
); @@ -50,6 +55,8 @@ function HorizontalTab({ HorizontalTab.defaultProps = { tabWidth: 8, + align: 'center', + theme: 'teal', }; export type TabItemProps = { @@ -58,42 +65,62 @@ export type TabItemProps = { to: string; active?: boolean; width?: string; + theme: 'teal' | 'gray'; }; -function TabItem({ name, text, to, active, width }: TabItemProps) { +function TabItem({ name, text, to, active, width, theme }: TabItemProps) { return ( - + {text} ); } -const Block = styled.div` +TabItem.defaultProps = { + theme: 'teal', +}; + +const Block = styled.div<{ align: 'center' | 'left' }>` display: flex; - justify-content: center; + ${props => + props.align === 'center' && + css` + justify-content: center; + `} .tab-wrapper { display: flex; position: relative; } `; -const Indicator = styled(animated.div)` +const Indicator = styled(animated.div)<{ theme: 'teal' | 'gray' }>` height: 2px; display: block; position: absolute; bottom: 0px; background: ${palette.teal5}; + ${props => + props.theme === 'gray' && + css` + background: ${palette.gray8}; + `} `; -const StyledLink = styled(Link)` +const StyledLink = styled(Link)<{ + theme: 'teal' | 'gray'; +}>` width: 8rem; height: 3rem; - font-size: 1.3125rem; + font-size: 1.125rem; ${media.small} { - height: 2.5rem; font-size: 1rem; } - color: ${palette.gray7}; + color: ${palette.gray6}; display: flex; align-items: center; justify-content: center; @@ -102,6 +129,11 @@ const StyledLink = styled(Link)` &.active { font-weight: bold; color: ${palette.teal5}; + ${props => + props.theme === 'gray' && + css` + color: ${palette.gray8}; + `} } `; diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 7edc27b0..192715b1 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -119,6 +119,7 @@ function filter(html: string) { 'iframe', 'span', 'img', + 'del', ], allowedAttributes: { a: ['href', 'name', 'target'], diff --git a/src/components/common/PostCard.tsx b/src/components/common/PostCard.tsx index 03ab089a..647abb32 100644 --- a/src/components/common/PostCard.tsx +++ b/src/components/common/PostCard.tsx @@ -16,9 +16,10 @@ import usePrefetchPost from '../../lib/hooks/usePrefetchPost'; export type PostCardProps = { post: PartialPost; + forHome?: boolean; }; -function PostCard({ post }: PostCardProps) { +function PostCard({ post, forHome }: PostCardProps) { const url = `/@${post.user.username}/${post.url_slug}`; const prefetch = usePrefetchPost(post.user.username, post.url_slug); @@ -35,7 +36,11 @@ function PostCard({ post }: PostCardProps) { }; return ( - + {post.thumbnail && ( +
@@ -136,7 +141,7 @@ const StyledLink = styled(Link)` text-decoration: none; `; -const Block = styled.div` +const Block = styled.div<{ forHome: boolean }>` width: 20rem; background: white; border-radius: 4px; @@ -153,6 +158,18 @@ const Block = styled.div` overflow: hidden; display: flex; flex-direction: column; + + ${props => + !props.forHome && + css` + ${mediaQuery(1440)} { + width: calc(25% - 2rem); + } + ${mediaQuery(1312)} { + width: calc(33% - 1.8125rem); + } + `} + ${mediaQuery(944)} { width: calc(50% - 2rem); } diff --git a/src/components/common/PostCardGrid.tsx b/src/components/common/PostCardGrid.tsx index 84416512..c0d98e08 100644 --- a/src/components/common/PostCardGrid.tsx +++ b/src/components/common/PostCardGrid.tsx @@ -6,14 +6,20 @@ import { mediaQuery } from '../../lib/styles/media'; export type PostCardGridProps = { posts: PartialPost[]; + loading?: boolean; + forHome?: boolean; }; -function PostCardGrid({ posts }: PostCardGridProps) { +function PostCardGrid({ posts, loading, forHome }: PostCardGridProps) { return ( {posts.map(post => ( - + ))} + {loading && + Array.from({ length: 8 }).map((_, i) => ( + + ))} ); } diff --git a/src/components/home/FloatingHomeHeader.tsx b/src/components/home/FloatingHomeHeader.tsx deleted file mode 100644 index cd8741a0..00000000 --- a/src/components/home/FloatingHomeHeader.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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/main/MainMobileHeadExtra.tsx b/src/components/home/HomeMobileHeadExtra.tsx similarity index 100% rename from src/components/main/MainMobileHeadExtra.tsx rename to src/components/home/HomeMobileHeadExtra.tsx diff --git a/src/components/main/MainNoticeWidget.tsx b/src/components/home/HomeNoticeWidget.tsx similarity index 89% rename from src/components/main/MainNoticeWidget.tsx rename to src/components/home/HomeNoticeWidget.tsx index 91b9aa7c..e6c9f8a7 100644 --- a/src/components/main/MainNoticeWidget.tsx +++ b/src/components/home/HomeNoticeWidget.tsx @@ -1,17 +1,17 @@ import * as React from 'react'; import styled from 'styled-components'; import palette from '../../lib/styles/palette'; -import MainWidget from './MainWidget'; +import HomeWidget from './HomeWidget'; import { Link } from 'react-router-dom'; import { PartialPost } from '../../lib/graphql/post'; import { formatDate } from '../../lib/utils'; import Skeleton from '../common/Skeleton'; -interface MainNoticeWidgetProps { +interface HomeNoticeWidgetProps { posts: PartialPost[]; } -const MainNoticeWidget: React.FC = ({ posts }) => { +const HomeNoticeWidget: React.FC = ({ posts }) => { const sliced = posts.slice(0, 5); return ( @@ -39,7 +39,7 @@ const MainNoticeWidget: React.FC = ({ posts }) => { ); }; -export function MainNoticeWidgetSkeleton() { +export function HomeNoticeWidgetSkeleton() { return (
    @@ -58,7 +58,7 @@ export function MainNoticeWidgetSkeleton() { ); } -const StyledWidget = styled(MainWidget)` +const StyledWidget = styled(HomeWidget)` .empty { padding-top: 1rem; padding-bottom: 1rem; @@ -105,4 +105,4 @@ const StyledWidget = styled(MainWidget)` } `; -export default MainNoticeWidget; +export default HomeNoticeWidget; diff --git a/src/components/main/MainRightFooter.tsx b/src/components/home/HomeRightFooter.tsx similarity index 89% rename from src/components/main/MainRightFooter.tsx rename to src/components/home/HomeRightFooter.tsx index 2652f138..1c5baafc 100644 --- a/src/components/main/MainRightFooter.tsx +++ b/src/components/home/HomeRightFooter.tsx @@ -27,9 +27,9 @@ const MainRightFooterBlock = styled.div` } `; -interface MainRightFooterProps {} +interface HomeRightFooterProps {} -const MainRightFooter: React.FC = props => { +const HomeRightFooter: React.FC = props => { return (
    @@ -51,4 +51,4 @@ const MainRightFooter: React.FC = props => { ); }; -export default MainRightFooter; +export default HomeRightFooter; diff --git a/src/components/home/HomeSidebar.tsx b/src/components/home/HomeSidebar.tsx index 9495daac..721144f5 100644 --- a/src/components/home/HomeSidebar.tsx +++ b/src/components/home/HomeSidebar.tsx @@ -1,8 +1,8 @@ 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 MainNoticeWidgetContainer from '../../containers/home/MainNoticeWidgetContainer'; +import MainTagWidgetContainer from '../../containers/home/MainTagWidgetContainer'; +import HomeRightFooter from './HomeRightFooter'; import Sticky from '../common/Sticky'; export type HomeSidebarProps = {}; @@ -13,7 +13,7 @@ function HomeSidebar(props: HomeSidebarProps) { - + ); diff --git a/src/components/home/HomeTab.tsx b/src/components/home/HomeTab.tsx index 86498a62..53b72dc4 100644 --- a/src/components/home/HomeTab.tsx +++ b/src/components/home/HomeTab.tsx @@ -6,7 +6,7 @@ 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'; +import HomeMobileHeadExtra from './HomeMobileHeadExtra'; export type HomeTabProps = {}; @@ -58,7 +58,7 @@ function HomeTab(props: HomeTabProps) { - + ); } @@ -100,7 +100,7 @@ const Block = styled.div` font-size: 1.125rem; text-decoration: none; color: ${palette.gray6}; - height: 2.875rem; + height: 3rem; svg { font-size: 1.5rem; diff --git a/src/components/main/MainTagWidget.tsx b/src/components/home/HomeTagWidget.tsx similarity index 84% rename from src/components/main/MainTagWidget.tsx rename to src/components/home/HomeTagWidget.tsx index ebf27711..d22d949e 100644 --- a/src/components/main/MainTagWidget.tsx +++ b/src/components/home/HomeTagWidget.tsx @@ -1,15 +1,15 @@ import React from 'react'; import styled from 'styled-components'; -import MainWidget from './MainWidget'; +import HomeWidget from './HomeWidget'; import palette from '../../lib/styles/palette'; import { Link } from 'react-router-dom'; import Skeleton from '../common/Skeleton'; -export type MainTagWidgetProps = { +export type HomeTagWidgetProps = { tags: string[]; }; -function MainTagWidget({ tags }: MainTagWidgetProps) { +function HomeTagWidget({ tags }: HomeTagWidgetProps) { return (
      @@ -26,7 +26,7 @@ function MainTagWidget({ tags }: MainTagWidgetProps) { ); } -export function MainTagWidgetSkeleton() { +export function HomeTagWidgetSkeleton() { return (
        @@ -40,7 +40,7 @@ export function MainTagWidgetSkeleton() { ); } -const StyledWidget = styled(MainWidget)` +const StyledWidget = styled(HomeWidget)` ul { list-style: none; padding-left: 0; @@ -72,4 +72,4 @@ const StyledWidget = styled(MainWidget)` } `; -export default MainTagWidget; +export default HomeTagWidget; diff --git a/src/components/home/HomeTemplate.tsx b/src/components/home/HomeTemplate.tsx deleted file mode 100644 index 5ace3bdb..00000000 --- a/src/components/home/HomeTemplate.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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/main/MainWidget.tsx b/src/components/home/HomeWidget.tsx similarity index 83% rename from src/components/main/MainWidget.tsx rename to src/components/home/HomeWidget.tsx index 821a9886..0927954e 100644 --- a/src/components/main/MainWidget.tsx +++ b/src/components/home/HomeWidget.tsx @@ -2,13 +2,13 @@ import React from 'react'; import styled from 'styled-components'; import palette from '../../lib/styles/palette'; -export type MainWidgetProps = { +export type HomeWidgetProps = { title: string; children: React.ReactNode; className?: string; }; -function MainWidget({ title, children, className }: MainWidgetProps) { +function HomeWidget({ title, children, className }: HomeWidgetProps) { return (

        {title}

        @@ -33,4 +33,4 @@ const MainWidgetBlock = styled.section` } `; -export default MainWidget; +export default HomeWidget; diff --git a/src/components/main/FloatingHomeHeader.tsx b/src/components/main/FloatingHomeHeader.tsx new file mode 100644 index 00000000..e22db2e8 --- /dev/null +++ b/src/components/main/FloatingHomeHeader.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useCallback, useRef, useState } from 'react'; +import styled from 'styled-components'; +import MainHeader from './MainHeader'; +import HomeTab from '../home/HomeTab'; +import MainResponsive from './MainResponsive'; +import { getScrollTop } from '../../lib/utils'; +import { Route } from 'react-router-dom'; +import ReadingListTab from '../readingList/ReadingListTab'; + +export type FloatingMainHeaderProps = {}; + +function FloatingMainHeader(props: FloatingMainHeaderProps) { + const [visible, setVisible] = useState(false); + const blockRef = useRef(null); + const [height, setHeight] = useState(0); + const [marginTop, setMarginTop] = useState(0); + useEffect(() => { + if (!blockRef.current) return; + setHeight(blockRef.current.clientHeight); + setMarginTop(-1 * blockRef.current.clientHeight); + }, []); + + 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 < -1 * height + ) { + transitionPoint.current = scrollTop + height; + } + + if (scrollTop < 64) { + setVisible(false); + } + + setMarginTop( + Math.min(0, -1 * height + transitionPoint.current - scrollTop), + ); + + direction.current = nextDirection; + prevScrollTop.current = scrollTop; + }, [height]); + + useEffect(() => { + document.addEventListener('scroll', onScroll); + return () => { + document.removeEventListener('scroll', onScroll); + }; + }, [onScroll]); + + return ( + + + ( +
        + + + +
        + )} + exact + /> + ( + + + + )} + exact + /> +
        + ); +} + +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; + } +`; + +const StyledMainResponsive = styled(MainResponsive)` + margin-top: 1.5rem; +`; + +export default FloatingMainHeader; diff --git a/src/components/home/HomeHeader.tsx b/src/components/main/MainHeader.tsx similarity index 85% rename from src/components/home/HomeHeader.tsx rename to src/components/main/MainHeader.tsx index 1a10527d..44f89710 100644 --- a/src/components/home/HomeHeader.tsx +++ b/src/components/main/MainHeader.tsx @@ -2,24 +2,26 @@ 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 MainResponsive from './MainResponsive'; +import useHeader from '../home/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 = {}; +export type MainHeaderProps = {}; -function HomeHeader(props: HomeHeaderProps) { +function MainHeader(props: MainHeaderProps) { const { user, onLoginClick, onLogout } = useHeader(); const [userMenu, toggleUserMenu] = useToggle(false); return ( - + + + {user ? ( @@ -62,6 +64,11 @@ const Block = styled.div` height: 4rem; `; +const StyledLink = styled(Link)` + display: flex; + align-items: center; +`; + const SearchButton = styled(Link)` display: flex; align-items: center; @@ -83,7 +90,7 @@ const SearchButton = styled(Link)` margin-right: 0.75rem; `; -const Inner = styled(HomeResponsive)` +const Inner = styled(MainResponsive)` height: 100%; display: flex; align-items: center; @@ -101,4 +108,4 @@ const Right = styled.div` } `; -export default HomeHeader; +export default MainHeader; diff --git a/src/components/main/MainMobileHead.tsx b/src/components/main/MainMobileHead.tsx deleted file mode 100644 index 0863f3bc..00000000 --- a/src/components/main/MainMobileHead.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { - MdTrendingUp, - MdAccessTime /*, MdRssFeed */, - MdMoreVert, -} from 'react-icons/md'; -import styled from 'styled-components'; -import { NavLink } from 'react-router-dom'; -import palette from '../../lib/styles/palette'; -import media from '../../lib/styles/media'; -import MainMobileHeadExtra from './MainMobileHeadExtra'; -import useToggle from '../../lib/hooks/useToggle'; - -export type MainMobileHeadProps = {}; - -function MainMobileHead(props: MainMobileHeadProps) { - 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(); - }; - - return ( -
        -
        - - ['/', '/trending'].includes(location.pathname) - } - > - - 트렌딩 - - - - 최신 - -
        -
        - -
        - -
        - ); -} - -const Section = styled.section` - position: relative; - display: none; - align-items: center; - justify-content: space-between; - width: 100%; - margin-bottom: 1.5rem; - .menu { - display: flex; - } - .more { - font-size: 1.5rem; - color: ${palette.gray6}; - } - - ${media.medium} { - display: flex; - } -`; - -const MenuItem = styled(NavLink)` - width: 5rem; - height: 2rem; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - font-weight: 600; - svg { - font-size: 1.125rem; - margin-right: 0.5rem; - } - font-size: 0.875rem; - border-bottom: 2px solid transparent; - color: ${palette.gray7}; - &.active { - background: ${palette.teal0}; - color: ${palette.teal6}; - border-bottom: 2px solid ${palette.teal6}; - } -`; - -export default MainMobileHead; diff --git a/src/components/main/MainPageTemplate.tsx b/src/components/main/MainPageTemplate.tsx new file mode 100644 index 00000000..966eaf87 --- /dev/null +++ b/src/components/main/MainPageTemplate.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import MainTemplate from './MainTemplate'; +import MainHeader from './MainHeader'; +import FloatingMainHeader from './FloatingHomeHeader'; + +export type MainPageTemplateProps = { + children?: React.ReactNode; +}; + +function MainPageTemplate({ children }: MainPageTemplateProps) { + return ( + + + + {children} + + ); +} + +export default MainPageTemplate; diff --git a/src/components/home/HomeResponsive.tsx b/src/components/main/MainResponsive.tsx similarity index 81% rename from src/components/home/HomeResponsive.tsx rename to src/components/main/MainResponsive.tsx index fff46d05..ae0d3205 100644 --- a/src/components/home/HomeResponsive.tsx +++ b/src/components/main/MainResponsive.tsx @@ -2,12 +2,12 @@ import React from 'react'; import styled from 'styled-components'; import { mediaQuery } from '../../lib/styles/media'; -export type HomeResponsiveProps = { +export type MainResponsiveProps = { className?: string; children: React.ReactNode; }; -function HomeResponsive({ className, children }: HomeResponsiveProps) { +function MainResponsive({ className, children }: MainResponsiveProps) { return {children}; } @@ -32,4 +32,4 @@ const Block = styled.div` } `; -export default HomeResponsive; +export default MainResponsive; diff --git a/src/components/main/MainSideMenu.tsx b/src/components/main/MainSideMenu.tsx deleted file mode 100644 index b8fd6a3b..00000000 --- a/src/components/main/MainSideMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from 'react'; -import styled from 'styled-components'; -import { NavLink, RouteComponentProps, withRouter } from 'react-router-dom'; -import palette from '../../lib/styles/palette'; -import { MdTrendingUp, MdAccessTime /*, MdRssFeed */ } from 'react-icons/md'; -import SearchInput from '../search/SearchInput'; - -const MainSideMenuBlock = styled.div` - position: sticky; - top: 96px; - display: flex; - flex-direction: column; -`; -const MenuItem = styled(NavLink)` - display: flex; - color: ${palette.gray8}; - text-decoration: none; - height: 48px; - align-items: center; - padding-left: 1rem; - font-size: 1.125rem; - border-left: 3px solid transparent; - transition: 0.125s all ease-in; - svg { - margin-right: 1rem; - font-size: 1.5rem; - } - &.active { - background: ${palette.teal0}; - border-color: ${palette.teal6}; - color: ${palette.teal6}; - font-weight: bold; - } -`; - -const StyledSearchInput = styled(SearchInput)` - margin-bottom: 1.5rem; -`; - -interface MainSideMenuProps extends RouteComponentProps {} - -const MainSideMenu: React.FC = ({ history }) => { - const onSearch = (keyword: string) => { - history.push(`/search/?q=${keyword}`); - }; - - return ( - - - { - return ['/', '/trending'].indexOf(location.pathname) !== -1; - }} - > - - 트렌딩 - - - - 최신 - - {/* - - 팔로잉 - */} - - ); -}; - -export default withRouter(MainSideMenu); diff --git a/src/components/main/MainTemplate.tsx b/src/components/main/MainTemplate.tsx index d35fe30a..95f5fc9e 100644 --- a/src/components/main/MainTemplate.tsx +++ b/src/components/main/MainTemplate.tsx @@ -1,73 +1,27 @@ -import * as React from 'react'; -import styled from 'styled-components'; -import PageTemplate from '../base/PageTemplate'; -import media from '../../lib/styles/media'; +import React from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import palette from '../../lib/styles/palette'; +import { Link } from 'react-router-dom'; -const MainTemplateBlock = styled(PageTemplate)` - main { - width: 1200px; - ${media.large} { - width: 1024px; - } - margin: 0 auto; - margin-top: 3.5rem; - margin-bottom: 8rem; - display: flex; - justify-content: space-between; - ${media.medium} { - justify-content: center; - width: 100%; - margin-top: 1rem; - } +const BackgroundStyle = createGlobalStyle` + body { + background: ${palette.gray0}; } `; -const Left = styled.div` - width: 192px; - ${media.medium} { - display: none; - } -`; -const Main = styled.div` - width: 702px; - ${media.large} { - width: 526px; - } - ${media.medium} { - width: 768px; - } - ${media.small} { - width: 100%; - padding-left: 1rem; - padding-right: 1rem; - } -`; -const Right = styled.div` - width: 240px; - ${media.medium} { - display: none; - } -`; - -type MainTemplateNamespace = { - Left: typeof Left; - Main: typeof Main; - Right: typeof Right; +export type MainTemplateProps = { + children: React.ReactNode; }; -interface MainTemplateProps {} -const MainTemplate: React.FC & MainTemplateNamespace = ({ - children, -}) => { +function MainTemplate({ children }: MainTemplateProps) { return ( - -
        {children}
        -
        + <> + + {children} + ); -}; +} -MainTemplate.Left = Left; -MainTemplate.Main = Main; -MainTemplate.Right = Right; +const Block = styled.div``; export default MainTemplate; diff --git a/src/components/readingList/ReadingListTab.tsx b/src/components/readingList/ReadingListTab.tsx new file mode 100644 index 00000000..ccd0aa70 --- /dev/null +++ b/src/components/readingList/ReadingListTab.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import HorizontalTab from '../common/HorizontalTab'; + +export type ReadingListTabProps = { + type: 'liked' | 'read'; +}; + +function ReadingListTab({ type }: ReadingListTabProps) { + return ( + + + + + ); +} + +export default ReadingListTab; diff --git a/src/containers/main/MainNoticeWidgetContainer.tsx b/src/containers/home/MainNoticeWidgetContainer.tsx similarity index 67% rename from src/containers/main/MainNoticeWidgetContainer.tsx rename to src/containers/home/MainNoticeWidgetContainer.tsx index e290582a..21542ee0 100644 --- a/src/containers/main/MainNoticeWidgetContainer.tsx +++ b/src/containers/home/MainNoticeWidgetContainer.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import MainNoticeWidget, { - MainNoticeWidgetSkeleton, -} from '../../components/main/MainNoticeWidget'; +import HomeNoticeWidget, { + HomeNoticeWidgetSkeleton, +} from '../../components/home/HomeNoticeWidget'; import { useQuery } from '@apollo/react-hooks'; import { GET_POST_LIST, PartialPost } from '../../lib/graphql/post'; @@ -15,9 +15,9 @@ function MainNoticeWidgetContainer(props: MainNoticeWidgetContainerProps) { }, }); - if (!data || !data.posts) return ; + if (!data || !data.posts) return ; - return ; + return ; } export default MainNoticeWidgetContainer; diff --git a/src/containers/main/MainTagWidgetContainer.tsx b/src/containers/home/MainTagWidgetContainer.tsx similarity index 75% rename from src/containers/main/MainTagWidgetContainer.tsx rename to src/containers/home/MainTagWidgetContainer.tsx index b375c065..49932616 100644 --- a/src/containers/main/MainTagWidgetContainer.tsx +++ b/src/containers/home/MainTagWidgetContainer.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import MainTagWidget, { - MainTagWidgetSkeleton, -} from '../../components/main/MainTagWidget'; +import HomeTagWidget, { + HomeTagWidgetSkeleton, +} from '../../components/home/HomeTagWidget'; import { useQuery } from '@apollo/react-hooks'; import { GET_TAGS, GetTagsResponse } from '../../lib/graphql/tags'; @@ -20,9 +20,9 @@ function MainTagWidgetContainer(props: MainTagWidgetContainerProps) { [data], ); - if (!tags) return ; + if (!tags) return ; - return ; + return ; } export default MainTagWidgetContainer; diff --git a/src/containers/main/RecentPosts.tsx b/src/containers/main/RecentPosts.tsx deleted file mode 100644 index 26147bd4..00000000 --- a/src/containers/main/RecentPosts.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useCallback } from 'react'; -import PostCardList, { - PostCardListSkeleton, -} 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'; -import { safe } from '../../lib/utils'; -import { Helmet } from 'react-helmet-async'; - -interface RecentPostsProps {} - -const RecentPosts: React.FC = props => { - const getPostList = useQuery<{ posts: PartialPost[] }>(GET_POST_LIST, { - notifyOnNetworkStatusChange: true, - }); - - const { data } = getPostList; - const onLoadMore = useCallback( - (cursor: string) => { - getPostList.fetchMore({ - variables: { - cursor, - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) return prev; - return { - posts: [...prev.posts, ...fetchMoreResult.posts], - }; - }, - }); - }, - [getPostList], - ); - - const cursor = safe(() => data!.posts[data!.posts.length - 1].id); - - useScrollPagination({ - cursor, - onLoadMore, - }); - - if (!data || !data.posts) return ; - - return ( - <> - - 최신 포스트 - velog - - - - {getPostList.loading && } - - ); -}; - -export default RecentPosts; diff --git a/src/containers/main/TrendingPosts.tsx b/src/containers/main/TrendingPosts.tsx deleted file mode 100644 index a2220d69..00000000 --- a/src/containers/main/TrendingPosts.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useCallback } from 'react'; -import PostCardList, { - PostCardListSkeleton, -} from '../../components/common/FlatPostCardList'; -import { - GET_TRENDING_POSTS, - GetTrendingPostsResponse, -} from '../../lib/graphql/post'; -import { useQuery } from '@apollo/react-hooks'; -import useScrollPagination from '../../lib/hooks/useScrollPagination'; -import { safe } from '../../lib/utils'; -import { Helmet } from 'react-helmet-async'; - -interface TrendingPostsProps {} - -const TrendingPosts: React.FC = props => { - const getTrendingPosts = useQuery( - GET_TRENDING_POSTS, - { notifyOnNetworkStatusChange: true }, - ); - - const { data, loading } = getTrendingPosts; - const onLoadMoreByOffset = useCallback( - (offset: number) => { - getTrendingPosts.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], - }; - }, - }); - }, - [getTrendingPosts], - ); - - const offset = safe(() => data!.trendingPosts.length); - - useScrollPagination({ - offset, - onLoadMoreByOffset, - }); - - if (!data || !data.trendingPosts) return ; - - return ( - <> - - - - - - - ); -}; - -export default TrendingPosts; diff --git a/src/lib/graphql/post.ts b/src/lib/graphql/post.ts index 03425d77..242ab3b6 100644 --- a/src/lib/graphql/post.ts +++ b/src/lib/graphql/post.ts @@ -704,8 +704,32 @@ export type PostViewResponse = { postView: boolean; }; -// mutation { -// createPostHistory(post_id: "41f19ed8-bc7e-4af3-86e9-4c5e482cb8e4", title: "안녕하세요", body: "내용입니다.", is_markdown:true) { -// id -// } -// } +export const GET_READING_LIST = gql` + query ReadingList($type: ReadingListOption, $cursor: ID, $limit: Int) { + readingList(type: $type, cursor: $cursor, limit: $limit) { + id + title + short_description + thumbnail + user { + id + username + profile { + id + thumbnail + } + } + url_slug + released_at + updated_at + comments_count + tags + is_private + likes + } + } +`; + +export type GetReadingListResponse = { + readingList: PartialPost[]; +}; diff --git a/src/lib/hooks/useScrollPagination.ts b/src/lib/hooks/useScrollPagination.ts index bb7d12d8..a90f2800 100644 --- a/src/lib/hooks/useScrollPagination.ts +++ b/src/lib/hooks/useScrollPagination.ts @@ -19,7 +19,6 @@ export default function useScrollPagination({ const last = useRef(null); const preventBottomStick = useCallback(() => { - console.log(getScrollBottom()); if (getScrollBottom() === 0) { window.scrollTo(0, getScrollTop() - 1); } @@ -43,7 +42,7 @@ export default function useScrollPagination({ const onScroll = useCallback(() => { const scrollBottom = getScrollBottom(); - if (scrollBottom < 768) { + if (scrollBottom < window.screen.height) { loadMore(); loadMoreUsingOffset(); } diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index f82f9741..e9151d7d 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -4,6 +4,7 @@ import LargeSearchInput from '../containers/search/LargeSearchInput'; import SearchResult from '../containers/search/SearchResult'; import { RouteComponentProps } from 'react-router'; import qs from 'qs'; +import { Helmet } from 'react-helmet-async'; export interface SearchPageProps extends RouteComponentProps {} @@ -17,6 +18,11 @@ function SearchPage({ location }: SearchPageProps) { return ( + + {(query.q || query.username) && ( + + )} + diff --git a/src/pages/getMatches.ts b/src/pages/getMatches.ts deleted file mode 100644 index 0d1af1a9..00000000 --- a/src/pages/getMatches.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { matchPath, RouteProps, match } from 'react-router'; -import MainPage from './main/MainPage'; - -const config: RouteProps[] = [ - { - path: '/', - component: MainPage, - }, -]; - -export function getMatches(path: string) { - return config - .map(r => { - const match = matchPath(path, r); - if (!match) return null; - return { - match, - component: r.component, - }; - }) - .filter(Boolean); -} diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index cd0cbdd4..121e1d67 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -1,23 +1,23 @@ import React from 'react'; -import HomeTemplate from '../../components/home/HomeTemplate'; -import HomeHeader from '../../components/home/HomeHeader'; +import MainTemplate from '../../components/main/MainTemplate'; +import MainHeader from '../../components/main/MainHeader'; import HomeTab from '../../components/home/HomeTab'; -import HomeResponsive from '../../components/home/HomeResponsive'; +import MainResponsive from '../../components/main/MainResponsive'; 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'; +import FloatingMainHeader from '../../components/main/FloatingHomeHeader'; export type HomePageProps = {}; function HomePage(props: HomePageProps) { return ( - - - - + + + + } /> - - + + ); } diff --git a/src/pages/home/RecentPostsPage.tsx b/src/pages/home/RecentPostsPage.tsx index a846077d..11460976 100644 --- a/src/pages/home/RecentPostsPage.tsx +++ b/src/pages/home/RecentPostsPage.tsx @@ -3,17 +3,27 @@ import useRecentPosts from './hooks/useRecentPosts'; import PostCardGrid, { PostCardGridSkeleton, } from '../../components/common/PostCardGrid'; +import { Helmet } from 'react-helmet-async'; export type RecentPostsPageProps = {}; function RecentPostsPage(props: RecentPostsPageProps) { const { data, loading } = useRecentPosts(); - if (!data) return ; return ( <> - - {loading && } + + 최신 포스트 - velog + + + ); } diff --git a/src/pages/home/TrendingPostsPage.tsx b/src/pages/home/TrendingPostsPage.tsx index a967dbd2..9adad876 100644 --- a/src/pages/home/TrendingPostsPage.tsx +++ b/src/pages/home/TrendingPostsPage.tsx @@ -3,18 +3,23 @@ import PostCardGrid, { PostCardGridSkeleton, } from '../../components/common/PostCardGrid'; import useTrendingPosts from './hooks/useTrendingPosts'; +import { Helmet } from 'react-helmet-async'; export type TrendingPageProps = {}; function TrendingPage(props: TrendingPageProps) { - const { data, loading, isFinished } = useTrendingPosts(); + const { data, loading } = useTrendingPosts(); - console.log(loading); - if (!data) return ; return ( <> - - {data && loading && } + + + + ); } diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx deleted file mode 100644 index c27341a5..00000000 --- a/src/pages/main/MainPage.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect } from 'react'; -import { Route, useLocation } from 'react-router-dom'; -import MainTemplate from '../../components/main/MainTemplate'; -import MainSideMenu from '../../components/main/MainSideMenu'; -import MainRightFooter from '../../components/main/MainRightFooter'; -import RecentPostsPage from './RecentPostsPage'; -import TrendingPostsPage from './TrendingPostsPage'; -import MainTagWidgetContainer from '../../containers/main/MainTagWidgetContainer'; -import MainNoticeWidgetContainer from '../../containers/main/MainNoticeWidgetContainer'; -import MainMobileHead from '../../components/main/MainMobileHead'; -import usePreserveScroll from '../../lib/hooks/usePreserveScroll'; -import { Helmet } from 'react-helmet-async'; - -interface MainPageProps {} - -const MainPage: React.FC = () => { - const { pathname } = useLocation(); - - // scroll to top when path changes - useEffect(() => { - window.scrollTo(0, 0); - }, [pathname]); - - usePreserveScroll('main'); - - return ( - - - - - - - - - - - - - - - - - - - ); -}; - -export default MainPage; diff --git a/src/pages/main/RecentPostsPage.tsx b/src/pages/main/RecentPostsPage.tsx deleted file mode 100644 index e03dc55c..00000000 --- a/src/pages/main/RecentPostsPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import styled from 'styled-components'; - -import RecentPosts from '../../containers/main/RecentPosts'; - -const RecentPostsPageBlock = styled.div``; - -interface RecentPostsPageProps {} - -const RecentPostsPage: React.FC = props => { - return ( - - - - ); -}; - -export default RecentPostsPage; diff --git a/src/pages/main/TrendingPostsPage.tsx b/src/pages/main/TrendingPostsPage.tsx deleted file mode 100644 index 2481c31a..00000000 --- a/src/pages/main/TrendingPostsPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import TrendingPosts from '../../containers/main/TrendingPosts'; - -export type TrendingPostsPageProps = {}; - -function TrendingPostsPage(props: TrendingPostsPageProps) { - return ; -} - -export default TrendingPostsPage; diff --git a/src/pages/readingList/ReadingListPage.tsx b/src/pages/readingList/ReadingListPage.tsx new file mode 100644 index 00000000..f991088e --- /dev/null +++ b/src/pages/readingList/ReadingListPage.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from 'react'; +import MainPageTemplate from '../../components/main/MainPageTemplate'; +import MainResponsive from '../../components/main/MainResponsive'; +import ReadingListTab from '../../components/readingList/ReadingListTab'; +import styled from 'styled-components'; +import { RouteComponentProps } from 'react-router-dom'; +import useReadingList from './hooks/useReadingList'; +import PostCardGrid, { + PostCardGridSkeleton, +} from '../../components/common/PostCardGrid'; +import { undrawEmpty } from '../../static/images'; +import palette from '../../lib/styles/palette'; +import media from '../../lib/styles/media'; +import { Helmet } from 'react-helmet-async'; + +export type ReadingListPageProps = {} & RouteComponentProps<{ + type: 'liked' | 'read'; +}>; + +function ReadingListPage({ match, history }: ReadingListPageProps) { + const { type } = match.params; + + const { data, loading, isFinished } = useReadingList(type); + + return ( + + + {} + 읽기 목록 - velog + + + + + + {data && data.readingList.length === 0 && ( + + list is empty +
        리스트가 비어있습니다.
        +
        + )} +
        +
        +
        + ); +} + +const StyledResponsive = styled(MainResponsive)` + margin-top: 1.5rem; +`; + +const Wrapper = styled.div` + margin-top: 2rem; +`; + +const EmptyWrapper = styled.div` + margin-top: 6rem; + align-items: center; + justify-content: center; + display: flex; + flex-direction: column; + img { + width: 25rem; + height: auto; + display: block; + margin-bottom: 2rem; + } + + .description { + color: ${palette.gray7}; + font-size: 1.5rem; + } + + ${media.small} { + margin-top: 3rem; + img { + max-width: 300px; + width: calc(100% - 2rem); + margin-bottom: 1rem; + } + .description { + font-size: 1.25rem; + } + } +`; + +export default ReadingListPage; diff --git a/src/pages/readingList/hooks/useReadingList.ts b/src/pages/readingList/hooks/useReadingList.ts new file mode 100644 index 00000000..6911fc94 --- /dev/null +++ b/src/pages/readingList/hooks/useReadingList.ts @@ -0,0 +1,63 @@ +import { + GET_READING_LIST, + GetReadingListResponse, +} from '../../../lib/graphql/post'; +import { useQuery } from '@apollo/react-hooks'; +import { useCallback, useState, useEffect } from 'react'; +import useScrollPagination from '../../../lib/hooks/useScrollPagination'; + +export default function useReadingList(type: 'liked' | 'read') { + const { data, loading, fetchMore } = useQuery( + GET_READING_LIST, + { + variables: { + type: type.toUpperCase(), + limit: 20, + }, + fetchPolicy: 'cache-and-network', + }, + ); + const [isFinished, setIsFinished] = useState(false); + + useEffect(() => { + setIsFinished(false); + }, [type]); + + useEffect(() => { + if (!data) return; + if (data.readingList.length < 20) { + setIsFinished(true); + } + }, [data]); + + const onLoadMore = useCallback( + (cursor: string) => { + fetchMore({ + variables: { + type: type.toUpperCase(), + cursor, + limit: 24, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + if (fetchMoreResult.readingList.length === 0) { + setIsFinished(true); + } + return { + readingList: [...prev.readingList, ...fetchMoreResult.readingList], + }; + }, + }); + }, + [fetchMore, type], + ); + + const cursor = data?.readingList[data?.readingList.length - 1]?.id; + + useScrollPagination({ + cursor, + onLoadMore, + }); + + return { data, loading, isFinished }; +}