From 922b0fa79391f6ab286db15ad4ebbea7771e287d Mon Sep 17 00:00:00 2001 From: Hyesung Oh Date: Thu, 31 Mar 2022 02:19:12 +0900 Subject: [PATCH] refactor: directory pattern at blog apps --- .../AuthorSection/AuthorSection.tsx | 64 ++++++++ .../src/components/AuthorSection/index.tsx | 65 +------- .../blog/src/components/Comments/Comments.tsx | 64 ++++++++ apps/blog/src/components/Comments/index.tsx | 65 +------- .../DateAndCategoryLink.tsx | 26 ++++ .../components/DateAndCategoryLink/index.tsx | 27 +--- .../blog/src/components/PostCard/PostCard.tsx | 47 ++++++ apps/blog/src/components/PostCard/index.tsx | 48 +----- apps/blog/src/components/SEO/SEO.tsx | 30 ++++ apps/blog/src/components/SEO/index.tsx | 31 +--- apps/blog/src/components/TOC/TOC.tsx | 138 +++++++++++++++++ apps/blog/src/components/TOC/index.tsx | 139 +----------------- 12 files changed, 375 insertions(+), 369 deletions(-) create mode 100644 apps/blog/src/components/AuthorSection/AuthorSection.tsx create mode 100644 apps/blog/src/components/Comments/Comments.tsx create mode 100644 apps/blog/src/components/DateAndCategoryLink/DateAndCategoryLink.tsx create mode 100644 apps/blog/src/components/PostCard/PostCard.tsx create mode 100644 apps/blog/src/components/SEO/SEO.tsx create mode 100644 apps/blog/src/components/TOC/TOC.tsx diff --git a/apps/blog/src/components/AuthorSection/AuthorSection.tsx b/apps/blog/src/components/AuthorSection/AuthorSection.tsx new file mode 100644 index 00000000..7d847fca --- /dev/null +++ b/apps/blog/src/components/AuthorSection/AuthorSection.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; +import { Avatar, Link } from '@nextui-org/react'; +import { KBarToggleButton } from 'core'; + +import { authorImage, authorName, defaultUrl } from 'core/constants'; +import { blogDescription } from '../../../_config'; + +interface Props { + marginBottom?: string; + hasKbarButton?: boolean; +} + +function AuthorSection({ marginBottom = '3.5rem', hasKbarButton = false }: Props) { + return ( +
+
+ + +

+ Personal blog by{' '} + + {authorName} + + . +

+

{blogDescription}

+
+
+ + {hasKbarButton && } +
+ ); +} + +export default AuthorSection; + +const Section = styled.section` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Div = styled.div` + display: flex; + align-items: center; +`; + +const TextWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 0.875rem; + + & > * { + margin: 0; + font-size: 1rem; + line-height: 1.5rem; + } +`; + +const H2 = styled.h2` + font-weight: normal; +`; diff --git a/apps/blog/src/components/AuthorSection/index.tsx b/apps/blog/src/components/AuthorSection/index.tsx index 7d847fca..e95a49c6 100644 --- a/apps/blog/src/components/AuthorSection/index.tsx +++ b/apps/blog/src/components/AuthorSection/index.tsx @@ -1,64 +1 @@ -import styled from '@emotion/styled'; -import { Avatar, Link } from '@nextui-org/react'; -import { KBarToggleButton } from 'core'; - -import { authorImage, authorName, defaultUrl } from 'core/constants'; -import { blogDescription } from '../../../_config'; - -interface Props { - marginBottom?: string; - hasKbarButton?: boolean; -} - -function AuthorSection({ marginBottom = '3.5rem', hasKbarButton = false }: Props) { - return ( -
-
- - -

- Personal blog by{' '} - - {authorName} - - . -

-

{blogDescription}

-
-
- - {hasKbarButton && } -
- ); -} - -export default AuthorSection; - -const Section = styled.section` - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; -`; - -const Div = styled.div` - display: flex; - align-items: center; -`; - -const TextWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 0.875rem; - - & > * { - margin: 0; - font-size: 1rem; - line-height: 1.5rem; - } -`; - -const H2 = styled.h2` - font-weight: normal; -`; +export { default } from './AuthorSection'; diff --git a/apps/blog/src/components/Comments/Comments.tsx b/apps/blog/src/components/Comments/Comments.tsx new file mode 100644 index 00000000..7afe73a8 --- /dev/null +++ b/apps/blog/src/components/Comments/Comments.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef } from 'react'; +import { useTheme } from '@nextui-org/react'; +import { blogRepo } from '../../../_config'; + +const src = 'https://utteranc.es/client.js'; +const LIGHT_THEME = 'github-light'; +const DARK_THEME = 'github-dark'; +const SYSTEM_THEME = 'preferred-color-scheme'; +const UTTERANCE_QUERY = '.utterances-frame'; + +function Comments() { + const rootElm = useRef(); + const { isDark } = useTheme(); + + // for initial + useEffect(() => { + function getCurrentTheme() { + const initialTheme = document.querySelector('html').classList[0]; + if (initialTheme) { + return initialTheme.includes('dark') ? DARK_THEME : LIGHT_THEME; + } + return SYSTEM_THEME; + } + + if (!rootElm.current) return; + if (document.querySelector(UTTERANCE_QUERY)) return; // prevant duplicate + + const utterances = document.createElement('script'); + const utterancesConfig = { + src, + repo: blogRepo, + theme: getCurrentTheme(), + label: 'comment', + async: true, + 'issue-term': 'pathname', + crossorigin: 'anonymous', + }; + + Object.keys(utterancesConfig).forEach(key => { + utterances.setAttribute(key, utterancesConfig[key]); + }); + rootElm.current.appendChild(utterances); + }, []); + + // for theme changed + useEffect(() => { + if (document.querySelector(UTTERANCE_QUERY)) { + const iframe = document.querySelector(UTTERANCE_QUERY); + + if (!iframe) { + return; + } + + iframe?.contentWindow?.postMessage( + { type: 'set-theme', theme: isDark ? DARK_THEME : LIGHT_THEME }, + 'https://utteranc.es' + ); + } + }, [isDark]); + + return
; +} + +export default Comments; diff --git a/apps/blog/src/components/Comments/index.tsx b/apps/blog/src/components/Comments/index.tsx index 7afe73a8..fff63290 100644 --- a/apps/blog/src/components/Comments/index.tsx +++ b/apps/blog/src/components/Comments/index.tsx @@ -1,64 +1 @@ -import { useEffect, useRef } from 'react'; -import { useTheme } from '@nextui-org/react'; -import { blogRepo } from '../../../_config'; - -const src = 'https://utteranc.es/client.js'; -const LIGHT_THEME = 'github-light'; -const DARK_THEME = 'github-dark'; -const SYSTEM_THEME = 'preferred-color-scheme'; -const UTTERANCE_QUERY = '.utterances-frame'; - -function Comments() { - const rootElm = useRef(); - const { isDark } = useTheme(); - - // for initial - useEffect(() => { - function getCurrentTheme() { - const initialTheme = document.querySelector('html').classList[0]; - if (initialTheme) { - return initialTheme.includes('dark') ? DARK_THEME : LIGHT_THEME; - } - return SYSTEM_THEME; - } - - if (!rootElm.current) return; - if (document.querySelector(UTTERANCE_QUERY)) return; // prevant duplicate - - const utterances = document.createElement('script'); - const utterancesConfig = { - src, - repo: blogRepo, - theme: getCurrentTheme(), - label: 'comment', - async: true, - 'issue-term': 'pathname', - crossorigin: 'anonymous', - }; - - Object.keys(utterancesConfig).forEach(key => { - utterances.setAttribute(key, utterancesConfig[key]); - }); - rootElm.current.appendChild(utterances); - }, []); - - // for theme changed - useEffect(() => { - if (document.querySelector(UTTERANCE_QUERY)) { - const iframe = document.querySelector(UTTERANCE_QUERY); - - if (!iframe) { - return; - } - - iframe?.contentWindow?.postMessage( - { type: 'set-theme', theme: isDark ? DARK_THEME : LIGHT_THEME }, - 'https://utteranc.es' - ); - } - }, [isDark]); - - return
; -} - -export default Comments; +export { default } from './Comments'; diff --git a/apps/blog/src/components/DateAndCategoryLink/DateAndCategoryLink.tsx b/apps/blog/src/components/DateAndCategoryLink/DateAndCategoryLink.tsx new file mode 100644 index 00000000..5f19e12f --- /dev/null +++ b/apps/blog/src/components/DateAndCategoryLink/DateAndCategoryLink.tsx @@ -0,0 +1,26 @@ +import NextLink from 'next/link'; +import { Link } from '@nextui-org/react'; + +interface Props { + date: string; + category?: string; +} + +function DateAndCategoryLink({ date, category }: Props) { + return ( + <> + {date} + {category && ( + <> + {' '} + at{' '} + + {category} + {' '} + category + + )} + + ); +} +export default DateAndCategoryLink; diff --git a/apps/blog/src/components/DateAndCategoryLink/index.tsx b/apps/blog/src/components/DateAndCategoryLink/index.tsx index 5f19e12f..6f0217f6 100644 --- a/apps/blog/src/components/DateAndCategoryLink/index.tsx +++ b/apps/blog/src/components/DateAndCategoryLink/index.tsx @@ -1,26 +1 @@ -import NextLink from 'next/link'; -import { Link } from '@nextui-org/react'; - -interface Props { - date: string; - category?: string; -} - -function DateAndCategoryLink({ date, category }: Props) { - return ( - <> - {date} - {category && ( - <> - {' '} - at{' '} - - {category} - {' '} - category - - )} - - ); -} -export default DateAndCategoryLink; +export { default } from './DateAndCategoryLink'; diff --git a/apps/blog/src/components/PostCard/PostCard.tsx b/apps/blog/src/components/PostCard/PostCard.tsx new file mode 100644 index 00000000..074af933 --- /dev/null +++ b/apps/blog/src/components/PostCard/PostCard.tsx @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; +import NextLink from 'next/link'; +import { Link, NextUITheme, theme } from '@nextui-org/react'; +import DateAndCategoryLink from '../DateAndCategoryLink'; + +interface Props { + slug: string; + title: string; + subtitle?: string; + date: string; + category?: string; + theme: typeof theme; +} + +function PostCard({ slug, title, subtitle, date, category, theme }: Props) { + return ( +
+

+ + + {title} + + +

+ + + + {subtitle &&

{subtitle}

} +
+ ); +} + +export default PostCard; + +const Article = styled.article` + width: 100%; + margin-bottom: 2.5rem; +`; + +const Small = styled.small<{ theme: NextUITheme | undefined }>` + color: ${({ theme }) => theme.colors.accents6.value}; +`; + +const P = styled.p` + width: 100%; + margin: 0; +`; diff --git a/apps/blog/src/components/PostCard/index.tsx b/apps/blog/src/components/PostCard/index.tsx index 074af933..0b99bf27 100644 --- a/apps/blog/src/components/PostCard/index.tsx +++ b/apps/blog/src/components/PostCard/index.tsx @@ -1,47 +1 @@ -import styled from '@emotion/styled'; -import NextLink from 'next/link'; -import { Link, NextUITheme, theme } from '@nextui-org/react'; -import DateAndCategoryLink from '../DateAndCategoryLink'; - -interface Props { - slug: string; - title: string; - subtitle?: string; - date: string; - category?: string; - theme: typeof theme; -} - -function PostCard({ slug, title, subtitle, date, category, theme }: Props) { - return ( -
-

- - - {title} - - -

- - - - {subtitle &&

{subtitle}

} -
- ); -} - -export default PostCard; - -const Article = styled.article` - width: 100%; - margin-bottom: 2.5rem; -`; - -const Small = styled.small<{ theme: NextUITheme | undefined }>` - color: ${({ theme }) => theme.colors.accents6.value}; -`; - -const P = styled.p` - width: 100%; - margin: 0; -`; +export { default } from './PostCard'; diff --git a/apps/blog/src/components/SEO/SEO.tsx b/apps/blog/src/components/SEO/SEO.tsx new file mode 100644 index 00000000..f3a571f2 --- /dev/null +++ b/apps/blog/src/components/SEO/SEO.tsx @@ -0,0 +1,30 @@ +import Head from 'next/head'; +import { authorName, defaultMetaBackground } from 'core/constants'; +import { blogName, blogDescription } from '../../../_config'; + +interface Props { + title?: string | undefined; + description?: string | undefined; + ogImage?: string | null; +} + +function SEO({ title, description, ogImage }: Props) { + const TITLE = title ? `${title} - ${authorName}` : `${blogName} - ${authorName}`; + const DESCRIPTION = description ? description : blogDescription; + + return ( + + {TITLE} + + + + + + {/* for twitter */} + + + + ); +} + +export default SEO; diff --git a/apps/blog/src/components/SEO/index.tsx b/apps/blog/src/components/SEO/index.tsx index f3a571f2..38d9fc1c 100644 --- a/apps/blog/src/components/SEO/index.tsx +++ b/apps/blog/src/components/SEO/index.tsx @@ -1,30 +1 @@ -import Head from 'next/head'; -import { authorName, defaultMetaBackground } from 'core/constants'; -import { blogName, blogDescription } from '../../../_config'; - -interface Props { - title?: string | undefined; - description?: string | undefined; - ogImage?: string | null; -} - -function SEO({ title, description, ogImage }: Props) { - const TITLE = title ? `${title} - ${authorName}` : `${blogName} - ${authorName}`; - const DESCRIPTION = description ? description : blogDescription; - - return ( - - {TITLE} - - - - - - {/* for twitter */} - - - - ); -} - -export default SEO; +export { default } from './SEO'; diff --git a/apps/blog/src/components/TOC/TOC.tsx b/apps/blog/src/components/TOC/TOC.tsx new file mode 100644 index 00000000..9b4b53d7 --- /dev/null +++ b/apps/blog/src/components/TOC/TOC.tsx @@ -0,0 +1,138 @@ +import { useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/router'; +import styled from '@emotion/styled'; +import { Link, NextUITheme, useTheme } from '@nextui-org/react'; +import { useMediaQuery } from 'core'; +import getHeadings from '../../utils/getHeadings'; + +function TOC() { + const [headings, setHeadings] = useState([]); + const router = useRouter(); + const { theme } = useTheme(); + + useEffect(() => { + setHeadings(getHeadings()); + }, [router]); + + const activeId = useScrollSpy({ + ids: headings, + options: { + rootMargin: '0% 0% -80% 0%', + }, + }); + + const isSmallToTOC = useMediaQuery(1000); + + if (headings.length <= 0 || isSmallToTOC) return <>; + + return ( + + ); +} + +export default TOC; + +const Aside = styled.aside` + position: sticky; + top: 5rem; +`; + +const Div = styled.div` + position: absolute; + padding-top: 0; + width: 280px; + + overflow: hidden; + top: 0; + left: calc(100% + 2.25rem); +`; + +const Ul = styled.ul` + width: 100%; + margin: 0; + padding-left: 1.25rem; + max-height: calc(100vh - 10rem); + overflow: auto; + + &::-webkit-scrollbar { + width: 0px; + } +`; + +const Li = styled.li` + width: 100%; + list-style-type: none; +`; + +const Anchor = styled(Link)<{ theme: NextUITheme | undefined }>` + position: relative; + color: ${({ theme }) => theme.colors.accents6.value}; + + &::before { + content: ''; + position: absolute; + display: inline-block; + top: 50%; + left: 0; + transform: translate(-300%, -50%); + width: 5px; + height: 5px; + border-radius: 10px; + background-color: ${({ theme }) => theme.colors.primary.value}; + + transition: opacity 0.3s; + opacity: 0; + } + + &.active { + color: ${({ theme }) => theme.colors.primary.value}; + + &::before { + opacity: 1; + } + } +`; + +interface HookProps { + ids: string[]; + options?: IntersectionObserverInit; +} + +function useScrollSpy({ ids, options }: HookProps) { + const [activeId, setActiveId] = useState(); + const observer = useRef(); + + useEffect(() => { + const elements = ids.map(id => document.querySelector(`#${id}`)); + + if (observer.current) { + observer.current.disconnect(); + } + + observer.current = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry?.isIntersecting) { + setActiveId(entry.target.getAttribute('id')); + } + }); + }, options); + + elements.forEach(el => el && observer.current?.observe(el)); + return () => observer.current?.disconnect(); + }, [ids, options]); + + return activeId; +} diff --git a/apps/blog/src/components/TOC/index.tsx b/apps/blog/src/components/TOC/index.tsx index 9b4b53d7..e7dc38bc 100644 --- a/apps/blog/src/components/TOC/index.tsx +++ b/apps/blog/src/components/TOC/index.tsx @@ -1,138 +1 @@ -import { useEffect, useRef, useState } from 'react'; -import { useRouter } from 'next/router'; -import styled from '@emotion/styled'; -import { Link, NextUITheme, useTheme } from '@nextui-org/react'; -import { useMediaQuery } from 'core'; -import getHeadings from '../../utils/getHeadings'; - -function TOC() { - const [headings, setHeadings] = useState([]); - const router = useRouter(); - const { theme } = useTheme(); - - useEffect(() => { - setHeadings(getHeadings()); - }, [router]); - - const activeId = useScrollSpy({ - ids: headings, - options: { - rootMargin: '0% 0% -80% 0%', - }, - }); - - const isSmallToTOC = useMediaQuery(1000); - - if (headings.length <= 0 || isSmallToTOC) return <>; - - return ( - - ); -} - -export default TOC; - -const Aside = styled.aside` - position: sticky; - top: 5rem; -`; - -const Div = styled.div` - position: absolute; - padding-top: 0; - width: 280px; - - overflow: hidden; - top: 0; - left: calc(100% + 2.25rem); -`; - -const Ul = styled.ul` - width: 100%; - margin: 0; - padding-left: 1.25rem; - max-height: calc(100vh - 10rem); - overflow: auto; - - &::-webkit-scrollbar { - width: 0px; - } -`; - -const Li = styled.li` - width: 100%; - list-style-type: none; -`; - -const Anchor = styled(Link)<{ theme: NextUITheme | undefined }>` - position: relative; - color: ${({ theme }) => theme.colors.accents6.value}; - - &::before { - content: ''; - position: absolute; - display: inline-block; - top: 50%; - left: 0; - transform: translate(-300%, -50%); - width: 5px; - height: 5px; - border-radius: 10px; - background-color: ${({ theme }) => theme.colors.primary.value}; - - transition: opacity 0.3s; - opacity: 0; - } - - &.active { - color: ${({ theme }) => theme.colors.primary.value}; - - &::before { - opacity: 1; - } - } -`; - -interface HookProps { - ids: string[]; - options?: IntersectionObserverInit; -} - -function useScrollSpy({ ids, options }: HookProps) { - const [activeId, setActiveId] = useState(); - const observer = useRef(); - - useEffect(() => { - const elements = ids.map(id => document.querySelector(`#${id}`)); - - if (observer.current) { - observer.current.disconnect(); - } - - observer.current = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry?.isIntersecting) { - setActiveId(entry.target.getAttribute('id')); - } - }); - }, options); - - elements.forEach(el => el && observer.current?.observe(el)); - return () => observer.current?.disconnect(); - }, [ids, options]); - - return activeId; -} +export { default } from './TOC';