mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-16 07:26:47 +00:00
@@ -38,8 +38,8 @@ const BLOG = {
|
||||
showPet: false, // 是否显示宠物挂件
|
||||
petLink: 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
|
||||
showToTop: true, // 是否显示回顶
|
||||
showToBottom: true, // 显示回底
|
||||
showDarkMode: true, // 显示日间/夜间模式切换
|
||||
showToBottom: false, // 显示回底
|
||||
showDarkMode: false, // 显示日间/夜间模式切换
|
||||
showToc: true, // 移动端显示悬浮目录
|
||||
showShareBar: false, // 文章分享功能
|
||||
showRelatePosts: true, // 相关文章推荐
|
||||
|
||||
@@ -13,10 +13,10 @@ export default function Analytics ({ postCount }) {
|
||||
<div className='mt-2 text-center dark:text-gray-300 font-light text-xs'>
|
||||
<span className='px-1 '>
|
||||
<strong className='font-medium'>{postCount}</strong>{locale.COMMON.POSTS}</span>
|
||||
{/* <span className='px-1 busuanzi_container_site_uv hidden'> */}
|
||||
{/* | <strong className='pl-1 busuanzi_value_site_uv font-medium'></strong>{locale.COMMON.VISITORS}</span> */}
|
||||
<span className='px-1 busuanzi_container_site_pv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_pv font-medium'></strong>{locale.COMMON.VIEWS}</span>
|
||||
<span className='px-1 busuanzi_container_site_uv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_uv font-medium'></strong>{locale.COMMON.VISITORS}</span>
|
||||
{/* <span className='px-1 busuanzi_container_site_pv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_pv font-medium'></strong>{locale.COMMON.VIEWS}</span> */}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import Comment from '@/components/Comment'
|
||||
import RecommendPosts from '@/components/RecommendPosts'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import TagItem from '@/components/TagItem'
|
||||
import TocDrawer from '@/components/TocDrawer'
|
||||
import TocDrawerButton from '@/components/TocDrawerButton'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
@@ -22,7 +20,6 @@ import 'prismjs/components/prism-typescript'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import ArticleCopyright from './ArticleCopyright'
|
||||
import Live2D from './Live2D'
|
||||
import WordCount from './WordCount'
|
||||
|
||||
/**
|
||||
@@ -31,8 +28,6 @@ import WordCount from './WordCount'
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
const targetRef = useRef(null)
|
||||
const drawerRight = useRef(null)
|
||||
const url = BLOG.link + useRouter().asPath
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
|
||||
@@ -55,144 +50,132 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
}
|
||||
})
|
||||
|
||||
return (<>
|
||||
<div id="container" ref={targetRef} className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
return (<div id="container" className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
|
||||
<header className='animate__slideInDown animate__animated'>
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
{/* <div className="w-full h-60 relative lg:h-96 transform duration-200 md:flex-shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={post?.page_cover}
|
||||
loading="eager"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
alt={post.title}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
|
||||
{post.type[0] !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
|
||||
{date}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
</>)}
|
||||
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<FontAwesomeIcon icon={faEye} className='mr-1'/>
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv"
|
||||
></span>
|
||||
<span className='mr-2'>|</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-nowrap whitespace-nowrap items-center font-light text-md'>
|
||||
<WordCount/>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{/* <hr className="mt-2" /> */}
|
||||
|
||||
</header>
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-1'>
|
||||
{post.blockMap && (
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
<header className='animate__slideInDown animate__animated'>
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
{/* <div className="w-full h-60 relative lg:h-96 transform duration-200 md:flex-shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={post?.page_cover}
|
||||
loading="eager"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
alt={post.title}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138"></ins>
|
||||
</section>
|
||||
|
||||
{/* 版权声明 */}
|
||||
<ArticleCopyright author={BLOG.author} url={url} />
|
||||
|
||||
{/* 推荐文章 */}
|
||||
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
|
||||
|
||||
{/* 标签列表 */}
|
||||
<section className="md:flex md:justify-between">
|
||||
{post.tagItems && (
|
||||
<div className="flex flex-nowrap leading-8 p-1 py-4 overflow-x-auto">
|
||||
<div className="hidden md:block dark:text-gray-300 whitespace-nowrap">
|
||||
{locale.COMMON.TAGS}:
|
||||
</div>
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItem key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ShareBar post={post} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BlogAround prev={prev} next={next} />
|
||||
|
||||
</article>
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="lg:px-40 md:hover:shadow-2xl duration-200 shadow w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<Comment frontMatter={post} />
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
|
||||
{post.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 悬浮目录按钮 */}
|
||||
<div className="block lg:hidden">
|
||||
<TocDrawerButton onClick={() => { drawerRight.current.handleSwitchVisible() }} />
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
|
||||
{/* 宠物 */}
|
||||
<Live2D/>
|
||||
{post.type[0] !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
|
||||
{date}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
</>)}
|
||||
|
||||
</>)
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<FontAwesomeIcon icon={faEye} className='mr-1'/>
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv"
|
||||
></span>
|
||||
<span className='mr-2'>|</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-nowrap whitespace-nowrap items-center font-light text-md'>
|
||||
<WordCount/>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{/* <hr className="mt-2" /> */}
|
||||
|
||||
</header>
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-1'>
|
||||
{post.blockMap && (
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138"></ins>
|
||||
</section>
|
||||
|
||||
{/* 版权声明 */}
|
||||
<ArticleCopyright author={BLOG.author} url={url} />
|
||||
|
||||
{/* 推荐文章 */}
|
||||
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
|
||||
|
||||
{/* 标签列表 */}
|
||||
<section className="md:flex md:justify-between">
|
||||
{post.tagItems && (
|
||||
<div className="flex flex-nowrap leading-8 p-1 py-4 overflow-x-auto">
|
||||
<div className="hidden md:block dark:text-gray-300 whitespace-nowrap">
|
||||
{locale.COMMON.TAGS}:
|
||||
</div>
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItem key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ShareBar post={post} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BlogAround prev={prev} next={next} />
|
||||
|
||||
</article>
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="lg:px-40 md:hover:shadow-2xl duration-200 shadow w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faAngleRight, faFolder } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faAngleDoubleRight, faFolder } from '@fortawesome/free-solid-svg-icons'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const showPreview = BLOG.home?.showPreview && post.blockMap
|
||||
return (
|
||||
<div key={post.id} className='shadow border animate__animated animate__fadeIn flex flex-col-reverse justify-between md:hover:shadow-xl duration-300
|
||||
w-full bg-white dark:bg-gray-800 dark:hover:bg-gray-700 dark:border-gray-600'>
|
||||
|
||||
<div className='lg:p-8 p-4 flex flex-col justify-between w-full'>
|
||||
<div className='lg:p-8 p-4 flex flex-col w-full'>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className='cursor-pointer font-bold text-3xl text-center leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400'>
|
||||
<a className={`cursor-pointer font-bold hover:underline text-3xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
|
||||
{post.title}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className='flex mt-2 items-center justify-center flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 '>
|
||||
<div className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'} flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 `}>
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className='cursor-pointer font-light text-sm hover:underline transform'>
|
||||
@@ -38,31 +39,31 @@ const BlogPostCard = ({ post, showSummary }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSummary && <p className='mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
|
||||
{(!showPreview || showSummary) && <p className='mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
|
||||
{post.summary}
|
||||
</p>}
|
||||
|
||||
{BLOG.home?.showPreview && post?.blockMap && <div className='max-h-screen overflow-hidden truncate max-w-full'>
|
||||
{showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'>
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
bodyClassName='max-h-full'
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
</div> }
|
||||
|
||||
<div className='border-b-2 w-full border-dashed py-2'></div>
|
||||
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<div className='flex items-center cursor-pointer pt-6 justify-end leading-tight'>
|
||||
<a className='bg-black p-2 text-white'>{locale.COMMON.ARTICLE_DETAIL}
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} /></a>
|
||||
</div>
|
||||
</Link>
|
||||
<div className='flex-col items-center justify-center article-cover'>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className='hover:bg-opacity-100 hover:scale-105 transform duration-300 rounded-md p-2 text-red-500 cursor-pointer'>
|
||||
{locale.COMMON.ARTICLE_DETAIL}
|
||||
<FontAwesomeIcon className='ml-1' icon={faAngleRight} /></a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{BLOG.home?.showPostCover && post?.page_cover && (
|
||||
|
||||
@@ -55,7 +55,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home
|
||||
return <div id='container' ref={targetRef}>
|
||||
|
||||
{/* 文章列表 */}
|
||||
<div className='flex flex-wrap space-y-8'>
|
||||
<div className='flex flex-wrap space-y-1 lg:space-y-4'>
|
||||
{postsToShow.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} showSummary={showSummary}/>
|
||||
))}
|
||||
@@ -65,7 +65,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home
|
||||
<div onClick={() => {
|
||||
handleGetMore()
|
||||
}}
|
||||
className='w-full my-4 py-4 text-center cursor-pointer glassmorphism shadow-xl rounded-xl dark:text-gray-200'
|
||||
className='w-full my-4 py-4 text-center cursor-pointer glassmorphism shadow-xl rounded-xl dark:text-gray-200'
|
||||
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`} </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
components/Collapse.js
Normal file
38
components/Collapse.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
const Collapse = props => {
|
||||
const collapseRef = useRef(null)
|
||||
const collapseSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = sectionHeight + 'px'
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = 0 + 'px'
|
||||
})
|
||||
})
|
||||
}
|
||||
const expandSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
element.style.height = sectionHeight + 'px'
|
||||
const clearTime = setTimeout(() => {
|
||||
element.style.height = 'auto'
|
||||
}, 400)
|
||||
clearTimeout(clearTime)
|
||||
}
|
||||
useEffect(() => {
|
||||
const element = collapseRef.current
|
||||
if (props.isOpen) {
|
||||
expandSection(element)
|
||||
} else {
|
||||
collapseSection(element)
|
||||
}
|
||||
}, [props.isOpen])
|
||||
return (
|
||||
<div ref={collapseRef} className=' overflow-hidden duration-200'>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Collapse.defaultProps = { isOpen: false }
|
||||
|
||||
export default Collapse
|
||||
@@ -1,26 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { loadUserThemeFromCookies, saveTheme, useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export default function FloatDarkModeButton () {
|
||||
if (!BLOG.widget?.showDarkMode) {
|
||||
return <></>
|
||||
}
|
||||
const [show, switchShow] = useState(false)
|
||||
const scrollListener = () => {
|
||||
const scrollY = window.pageYOffset
|
||||
const shouldShow = scrollY > 100
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
scrollListener()
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
const { changeTheme } = useGlobal()
|
||||
const userTheme = loadUserThemeFromCookies()
|
||||
@@ -36,12 +23,8 @@ export default function FloatDarkModeButton () {
|
||||
|
||||
return (
|
||||
<div
|
||||
id='float-dark-mode-button'
|
||||
onClick={handleChangeDarkMode}
|
||||
className={
|
||||
(show ? '' : ' hidden ') +
|
||||
' animate__fadeInRight animate__animated animate__faster fixed right-1 bottom-28 z-10 duration-500 text-xs cursor-pointer ' +
|
||||
' text-black dark:border-gray-500 flex justify-center items-center w-8 h-8 glassmorphism dark:bg-gray-700 dark:text-gray-200'
|
||||
className={ ' text-black dark:border-gray-500 flex justify-center items-center w-8 h-8 dark:text-gray-200'
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
@@ -18,7 +17,6 @@ const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const [show, switchShow] = useState(false)
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
@@ -47,15 +45,12 @@ const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<div id='jump-to-top' className='right-1 fixed flex bottom-36 z-20'>
|
||||
<div onClick={() => scrollToBottom()}
|
||||
className={(show ? '' : 'hidden') + ' animate__fadeInRight duration-500 animate__animated animate__faster glassmorphism flex justify-center items-center w-8 h-8 cursor-pointer '}>
|
||||
<div className='dark:text-gray-200 transform hover:scale-150 text-xs duration-200' title={locale.POST.TOP} >
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</div>
|
||||
{showPercent && (<div className='w-10 text-xs dark:text-gray-200'>{percent}</div>)}
|
||||
</div>
|
||||
</div>)
|
||||
return (<div className='flex space-x-1 transform hover:scale-105 text-xs duration-200 py-2 px-3' onClick={scrollToBottom} >
|
||||
<div className='dark:text-gray-200' >
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</div>
|
||||
{showPercent && (<div className='text-xs dark:text-gray-200 block lg:hidden'>{percent}%</div>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default JumpToBottomButton
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
@@ -13,41 +12,16 @@ import BLOG from '@/blog.config'
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = false }) => {
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
if (!BLOG.widget?.showToTop) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
const [show, switchShow] = useState(false)
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
const clientHeight = targetRef?.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight * 100)).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
const shouldShow = scrollY > 100 && per > 0
|
||||
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
changePercent(per)
|
||||
}
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<div id='jump-to-top' className='right-1 fixed flex bottom-44 z-20'>
|
||||
<div onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
className={(show ? '' : 'hidden') + ' animate__fadeInRight duration-500 animate__animated animate__faster flex justify-center items-center w-8 h-8 glassmorphism cursor-pointer '}>
|
||||
<div className='dark:text-gray-200 transform hover:scale-150 text-xs duration-200' title={locale.POST.TOP} >
|
||||
return (<div className='flex space-x-1 transform hover:scale-105 text-xs duration-200 py-2 px-3' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
|
||||
<div className='dark:text-gray-200' title={locale.POST.TOP} >
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</div>
|
||||
{showPercent && (<div className='w-10 text-xs dark:text-gray-200'>{percent}</div>)}
|
||||
</div>
|
||||
{showPercent && (<div className='text-xs dark:text-gray-200 block lg:hidden'>{percent}%</div>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,27 +6,30 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArchive, faHome, faTag, faTh, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import BLOG from 'blog.config'
|
||||
|
||||
const MenuButtonGroup = ({ allowCollapse = false }) => {
|
||||
const MenuButtonGroup = ({ allowCollapse = false, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const archiveSlot = <div className='bg-gray-300 dark:bg-gray-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
|
||||
|
||||
const links = [
|
||||
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ id: 1, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', show: BLOG.menu.showArchive },
|
||||
{ id: 2, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.menu.showCategory },
|
||||
{ id: 3, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.menu.showTag },
|
||||
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.menu.showCategory },
|
||||
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.menu.showTag },
|
||||
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: BLOG.menu.showArchive },
|
||||
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: BLOG.menu.showAbout }
|
||||
]
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 '>
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
||||
<a className={'py-1 my-1 px-5 mx-2 duration-300 text-base hover:bg-gray-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
|
||||
<a className={'py-2 px-5 mx-2 duration-300 text-base justify-between hover:bg-gray-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
|
||||
(selected ? 'bg-gray-200 text-black' : ' ')} >
|
||||
<div className='my-auto justify-center flex '>
|
||||
<FontAwesomeIcon icon={link.icon} />
|
||||
<div className={'ml-4'}>{link.name}</div>
|
||||
</div>
|
||||
<div className={'ml-4'}>{link.name}</div>
|
||||
{link.slot}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
|
||||
@@ -6,10 +6,11 @@ import React, { useEffect, useState } from 'react'
|
||||
* @constructor
|
||||
*/
|
||||
const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
if (targetRef?.current) {
|
||||
const clientHeight = targetRef ? (targetRef.current.clientHeight) : 0
|
||||
if (currentRef) {
|
||||
const clientHeight = currentRef ? (currentRef.clientHeight) : 0
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight * 100)).toFixed(0))
|
||||
@@ -24,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
|
||||
return (<div className='h-4 w-full shadow-2xl bg-gray-400'>
|
||||
return (<div className='h-4 w-full shadow-2xl bg-gray-400 font-sans'>
|
||||
<div className='h-4 bg-gray-600 duration-200' style={{ width: `${percent}%` }}>
|
||||
{showPercent && <div className='text-right text-white text-xs'>{percent}%</div>}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'next/router'
|
||||
import { useImperativeHandle, useRef } from 'react'
|
||||
import SearchInput from './SearchInput'
|
||||
const SearchDrawer = ({ cRef }) => {
|
||||
const SearchDrawer = ({ cRef, slot }) => {
|
||||
const searchDrawer = useRef()
|
||||
const searchInputRef = useRef()
|
||||
useImperativeHandle(cRef, () => {
|
||||
@@ -20,14 +20,15 @@ const SearchDrawer = ({ cRef }) => {
|
||||
})
|
||||
return (
|
||||
<div id='search-drawer-wrapper' ref={searchDrawer} className='hidden'>
|
||||
<div className='flex absolute px-5 w-full h-full left-0 top-14 z-50 justify-center'>
|
||||
<div className='flex-col fixed px-5 w-full left-0 top-14 z-50 justify-center'>
|
||||
<div className='md:max-w-3xl w-full mx-auto animate__animated animate__faster animate__fadeIn'>
|
||||
<SearchInput cRef={searchInputRef} />
|
||||
<SearchInput cRef={searchInputRef} />
|
||||
{slot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 背景蒙版 */}
|
||||
<div id='search-drawer-background' onClick={hidden} className='animate__animated animate__faster animate__fadeIn fixed glassmorphism top-0 left-0 z-30 w-full h-full' />
|
||||
<div id='search-drawer-background' onClick={hidden} className='animate__animated animate__faster animate__fadeIn fixed bg-day dark:bg-night top-0 left-0 z-40 w-full h-full' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch }) => {
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
const { locale } = useGlobal()
|
||||
const [searchKey, setSearchKey] = useState(currentSearch)
|
||||
const [searchKey, setSearchKey] = useState(currentSearch || '')
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef()
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleSearch = (key) => {
|
||||
if (key && key !== '') {
|
||||
setLoadingState(true)
|
||||
@@ -45,7 +52,7 @@ const SearchInput = ({ currentTag, currentSearch }) => {
|
||||
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 border-gray-300 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={currentSearch}
|
||||
defaultValue={searchKey}
|
||||
/>
|
||||
{(searchKey && searchKey.length && <FontAwesomeIcon icon={faTimes} className='text-gray-300 float-right m-3 cursor-pointer' onClick={cleanSearch} />)}
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ const SideAreaLeft = ({ title, tags, currentTag, post, postCount, categories, cu
|
||||
{/* 菜单 */}
|
||||
<section className='shadow hidden lg:block mb-5 pb-4 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
|
||||
<Logo/>
|
||||
<div className='pt-2'>
|
||||
<MenuButtonGroup allowCollapse={true} />
|
||||
<div className='pt-2 font-sans'>
|
||||
<MenuButtonGroup allowCollapse={true} postCount={postCount} />
|
||||
</div>
|
||||
{BLOG.menu.showSearch && <div className='px-5 pt-2'>
|
||||
{BLOG.menu.showSearch && <div className='px-5 pt-2 font-sans'>
|
||||
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
|
||||
</div>}
|
||||
</section>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
@@ -13,6 +14,9 @@ let windowTop = 0
|
||||
const StickyBar = ({ children }) => {
|
||||
if (!children) return <></>
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
if (BLOG.topNavType === 'normal') {
|
||||
return
|
||||
}
|
||||
const scrollS = window.scrollY
|
||||
if (scrollS >= windowTop && scrollS > 10) {
|
||||
const stickyBar = document.querySelector('#sticky-bar')
|
||||
@@ -35,7 +39,7 @@ const StickyBar = ({ children }) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id='sticky-bar' className='sticky flex-grow justify-center top-14 md:top-0 duration-500 z-10 pb-16'>
|
||||
<div id='sticky-bar' className='sticky flex-grow justify-center top-0 duration-500 z-10 pb-16'>
|
||||
<div className='glassmorphism dark:border-gray-600 px-5 absolute shadow-md border w-full hidden-scroll'>
|
||||
<div id='tag-container' className="md:pl-3 overflow-x-auto">
|
||||
{ children }
|
||||
|
||||
@@ -8,7 +8,7 @@ const TagItemMini = ({ tag, selected = false }) => {
|
||||
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
|
||||
${selected
|
||||
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-gray-900'
|
||||
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-900`}` }>
|
||||
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}` }>
|
||||
<div className='font-light dark:text-gray-400'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -56,7 +56,7 @@ const Toc = ({ toc, targetRef }) => {
|
||||
<div className='w-full'>
|
||||
<Progress targetRef={targetRef}/>
|
||||
</div>
|
||||
<nav className=' overflow-y-auto scroll-hidden'>
|
||||
<nav className='font-sans overflow-y-auto scroll-hidden'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faListOl } from '@fortawesome/free-solid-svg-icons'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faListOl } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 点击召唤目录抽屉
|
||||
@@ -16,30 +16,9 @@ const TocDrawerButton = (props) => {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
const [show, switchShow] = useState(false)
|
||||
const scrollListener = () => {
|
||||
const scrollY = window.pageYOffset
|
||||
const shouldShow = scrollY > 100
|
||||
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
})
|
||||
|
||||
return (
|
||||
<div id='toc-drawer-button' className='right-1 fixed bottom-52 z-20'>
|
||||
<div onClick={props.onClick} className={(show ? 'animate__fadeInRight' : 'hidden') + ' animate__animated animate__faster glassmorphism cursor-pointer' }>
|
||||
<div className='dark:text-gray-200 text-center transform hover:scale-150 duration-200 text-xs flex justify-center items-center w-8 h-8' title={locale.POST.TOP} >
|
||||
<FontAwesomeIcon icon={faListOl}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
return (<div onClick={props.onClick} className='px-3 py-2 cursor-pointer dark:text-gray-200 text-center transform hover:scale-150 duration-200 text-xs flex justify-center items-center' title={locale.POST.TOP} >
|
||||
<FontAwesomeIcon icon={faListOl}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TocDrawerButton
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import SideBarDrawer from '@/components/SideBarDrawer'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faBars, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faAngleDoubleRight, faBars, faSearch, faTag, faThList, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import Collapse from './Collapse'
|
||||
import Logo from './Logo'
|
||||
import MenuButtonGroup from './MenuButtonGroup'
|
||||
import SearchDrawer from './SearchDrawer'
|
||||
import TagGroups from './TagGroups'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
@@ -15,8 +19,7 @@ let windowTop = 0
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, autoHide = true }) => {
|
||||
const drawer = useRef()
|
||||
const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, autoHide = true, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
const searchDrawer = useRef()
|
||||
|
||||
@@ -43,19 +46,55 @@ const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, aut
|
||||
BLOG.autoCollapsedNavBar && window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
const searchDrawerSlot = <>
|
||||
{ categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm px-5 flex flex-nowrap justify-between font-light'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faThList} className='mr-2' />{locale.COMMON.CATEGORY}</div>
|
||||
<Link href='/category' passHref>
|
||||
<a className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</section>
|
||||
) }
|
||||
|
||||
{ tags && (
|
||||
<section className='mt-4'>
|
||||
<div className='text-sm py-2 px-5 flex flex-nowrap justify-between font-light dark:text-gray-200'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faTag} className='mr-2'/>{locale.COMMON.TAGS}</div>
|
||||
<Link href='/tag' passHref>
|
||||
<a className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='px-5 py-2'>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</section>
|
||||
) }
|
||||
</>
|
||||
|
||||
return (<div id='top-nav' className='z-40 block lg:hidden'>
|
||||
{/* 侧面抽屉 */}
|
||||
<SideBarDrawer post={post} currentTag={currentTag} cRef={drawer} tags={tags} slot={slot} categories={categories} currentCategory={currentCategory}/>
|
||||
<SearchDrawer cRef={searchDrawer}/>
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={`${BLOG.topNavType !== 'normal' ? 'fixed' : ''} flex animate__animated animate__fadeIn lg:relative w-full top-0 z-20 transform duration-500`}>
|
||||
<div id='sticky-nav' className={`${BLOG.topNavType !== 'normal' ? 'fixed' : ''} animate__animated animate__fadeIn lg:relative w-full top-0 z-20 transform duration-500`}>
|
||||
<div className='w-full flex justify-between items-center p-4 bg-black text-white'>
|
||||
{/* 左侧LOGO 标题 */}
|
||||
<div className='flex flex-none flex-grow-0'>
|
||||
<div onClick={() => { drawer.current.handleSwitchSideDrawerVisible() }}
|
||||
className='w-8 cursor-pointer'>
|
||||
<FontAwesomeIcon icon={faBars} size={'lg'}/>
|
||||
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
|
||||
{ isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +109,12 @@ const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, aut
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapse isOpen={isOpen}>
|
||||
<div className='bg-white py-1'>
|
||||
<MenuButtonGroup postCount={postCount}/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
</div>)
|
||||
|
||||
@@ -10,7 +10,9 @@ import SideAreaRight from '@/components/SideAreaRight'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param children
|
||||
@@ -38,6 +40,7 @@ const BaseLayout = ({
|
||||
post,
|
||||
postCount,
|
||||
sideBarSlot,
|
||||
floatSlot,
|
||||
rightAreaSlot,
|
||||
currentSearch,
|
||||
currentCategory,
|
||||
@@ -48,19 +51,41 @@ const BaseLayout = ({
|
||||
const { onLoading } = useGlobal()
|
||||
const targetRef = useRef(null)
|
||||
|
||||
const [show, switchShow] = useState(false)
|
||||
const [percent, changePercent] = useState(0) // 页面阅读百分比
|
||||
const scrollListener = () => {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
const clientHeight = targetRef?.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight * 100)).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
const shouldShow = scrollY > 100 && per > 0
|
||||
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
changePercent(per)
|
||||
}
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<>
|
||||
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
<TopNav tags={tags} post={post} slot={sideBarSlot} currentSearch={currentSearch} categories={categories} currentCategory={currentCategory} />
|
||||
<TopNav tags={tags} postCount={postCount} post={post} slot={sideBarSlot} currentSearch={currentSearch} categories={categories} currentCategory={currentCategory} />
|
||||
|
||||
<>{headerSlot}</>
|
||||
|
||||
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block'></div>
|
||||
|
||||
<main id='wrapper' className='flex justify-center flex-1 mx-auto pb-12'>
|
||||
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
|
||||
<SideAreaLeft targetRef={targetRef} post={post} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
|
||||
<section id='center' className={`${BLOG.topNavType !== 'normal' ? 'mt-14' : ''} flex-grow md:mt-0 max-w-5xl min-h-screen w-full`} ref={targetRef}>
|
||||
<section id='center' className={`${BLOG.topNavType !== 'normal' ? 'mt-14' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
|
||||
{onLoading
|
||||
? <LoadingCover/>
|
||||
: <>
|
||||
@@ -71,10 +96,16 @@ const BaseLayout = ({
|
||||
<SideAreaRight targetRef={targetRef} post={post} slot={rightAreaSlot} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
|
||||
</main>
|
||||
|
||||
<div className='right-4 lg:right-2 bottom-2 fixed justify-end z-20 rounded font-sans'>
|
||||
<div className={(show ? 'animate__animated ' : 'hidden') + ' animate__fadeInUp glassmorphism justify-center duration-500 animate__faster flex space-x-2 items-center cursor-pointer '}>
|
||||
<JumpToTopButton percent={percent}/>
|
||||
<JumpToBottomButton />
|
||||
<FloatDarkModeButton/>
|
||||
{floatSlot}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<Footer title={meta.title}/>
|
||||
<JumpToTopButton showPercent={false} />
|
||||
<JumpToBottomButton showPercent={false}/>
|
||||
<FloatDarkModeButton/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import lang from './lang'
|
||||
import { useContext, createContext, useState, useEffect } from 'react'
|
||||
import cookie from 'react-cookies'
|
||||
import Router from 'next/router'
|
||||
import { initTheme, loadUserThemeFromCookies } from './theme'
|
||||
const GlobalContext = createContext()
|
||||
|
||||
/**
|
||||
@@ -76,48 +76,6 @@ const initLocale = (locale, changeLocale) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化主题
|
||||
* @param theme 用户默认主题state
|
||||
* @param changeTheme 更改主题ChangeState函数
|
||||
* @description 读取cookie中存的用户主题
|
||||
*/
|
||||
const initTheme = (theme, changeTheme) => {
|
||||
if (!theme) {
|
||||
const date = new Date()
|
||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const useDark = prefersDarkMode || (date.getHours() >= 18 || date.getHours() < 6)
|
||||
const htmlElement = document.getElementsByTagName('html')
|
||||
|
||||
if (useDark) {
|
||||
changeTheme('dark')
|
||||
saveTheme('dark')
|
||||
htmlElement.classList?.remove('light')
|
||||
htmlElement.classList?.add('dark')
|
||||
} else {
|
||||
changeTheme('light')
|
||||
saveTheme('light')
|
||||
htmlElement.classList?.remove('dark')
|
||||
htmlElement.classList?.add('light')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取默认主题
|
||||
* @returns {*}
|
||||
*/
|
||||
export const loadUserThemeFromCookies = () => {
|
||||
return cookie.load('theme')
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存默认主题
|
||||
* @param newTheme
|
||||
*/
|
||||
export const saveTheme = (newTheme) => {
|
||||
cookie.save('theme', newTheme, { path: '/' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并两个对象
|
||||
|
||||
@@ -2,12 +2,12 @@ import BLOG from '@/blog.config'
|
||||
import { NotionAPI } from 'notion-client'
|
||||
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
|
||||
|
||||
export async function getPostBlocks (id, from) {
|
||||
export async function getPostBlocks (id, from, slice) {
|
||||
const cacheKey = 'page_block_' + id
|
||||
let pageBlock = await getDataFromCache(cacheKey)
|
||||
if (pageBlock) {
|
||||
console.log('[请求缓存]:', `from:${from}`, `id:${id}`)
|
||||
return pageBlock
|
||||
return filterPostBlocks(id, pageBlock, slice)
|
||||
}
|
||||
const authToken = BLOG.notionAccessToken || null
|
||||
const api = new NotionAPI({ authToken })
|
||||
@@ -20,24 +20,57 @@ export async function getPostBlocks (id, from) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 去掉不用的字段
|
||||
for (const j in pageBlock?.block) {
|
||||
const b = pageBlock?.block[j]
|
||||
if (b) {
|
||||
delete b.role
|
||||
delete b.value?.version
|
||||
delete b.value?.created_time
|
||||
delete b.value?.last_edited_time
|
||||
delete b.value?.created_by_table
|
||||
delete b.value?.created_by_id
|
||||
delete b.value?.last_edited_by_table
|
||||
delete b.value?.last_edited_by_id
|
||||
delete b.value?.space_id
|
||||
}
|
||||
}
|
||||
|
||||
if (pageBlock) {
|
||||
await setDataToCache(cacheKey, pageBlock)
|
||||
return filterPostBlocks(id, pageBlock, slice)
|
||||
}
|
||||
return pageBlock
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} id 页面ID
|
||||
* @param {*} pageBlock 页面元素
|
||||
* @param {*} slice 截取数量
|
||||
* @returns
|
||||
*/
|
||||
function filterPostBlocks (id, pageBlock, slice) {
|
||||
const clonePageBlock = deepClone(pageBlock)
|
||||
let count = 0
|
||||
|
||||
for (const i in clonePageBlock?.block) {
|
||||
const b = clonePageBlock?.block[i]
|
||||
if (slice && slice > 0 && count > slice) {
|
||||
delete clonePageBlock?.block[i]
|
||||
continue
|
||||
}
|
||||
count++
|
||||
delete b?.role
|
||||
delete b?.value?.version
|
||||
delete b?.value?.created_time
|
||||
delete b?.value?.last_edited_time
|
||||
delete b?.value?.created_by_table
|
||||
delete b?.value?.created_by_id
|
||||
delete b?.value?.last_edited_by_table
|
||||
delete b?.value?.last_edited_by_id
|
||||
delete b?.value?.space_id
|
||||
}
|
||||
|
||||
// 去掉不用的字段
|
||||
if (id === BLOG.notionPageId) {
|
||||
return clonePageBlock
|
||||
}
|
||||
return clonePageBlock
|
||||
}
|
||||
|
||||
function deepClone (obj) {
|
||||
const newObj = Array.isArray(obj) ? [] : {}
|
||||
if (obj && typeof obj === 'object') {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
44
lib/theme.js
Normal file
44
lib/theme.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import cookie from 'react-cookies'
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
* @param theme 用户默认主题state
|
||||
* @param changeTheme 更改主题ChangeState函数
|
||||
* @description 读取cookie中存的用户主题
|
||||
*/
|
||||
export const initTheme = (theme, changeTheme) => {
|
||||
// 若未指定主题,则从时间和浏览器偏好中决定初始主题
|
||||
if (!theme) {
|
||||
const date = new Date()
|
||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const useDark = prefersDarkMode || (date.getHours() >= 18 || date.getHours() < 6)
|
||||
if (useDark) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const htmlElement = document.getElementsByTagName('html')
|
||||
htmlElement.className = ''
|
||||
changeTheme(theme)
|
||||
saveTheme(theme)
|
||||
htmlElement.classList?.add(theme)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取默认主题
|
||||
* @returns {*}
|
||||
*/
|
||||
export const loadUserThemeFromCookies = () => {
|
||||
return cookie.load('theme')
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存默认主题
|
||||
* @param newTheme
|
||||
*/
|
||||
export const saveTheme = (newTheme) => {
|
||||
cookie.save('theme', newTheme, { path: '/' })
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import React from 'react'
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const About = ({ post, blockMap, tags, prev, next, allPosts, categories }) => {
|
||||
const About = ({ post, blockMap, tags, prev, next, postCount, categories }) => {
|
||||
if (!post) {
|
||||
return <Custom404 />
|
||||
}
|
||||
@@ -39,10 +39,10 @@ const About = ({ post, blockMap, tags, prev, next, allPosts, categories }) => {
|
||||
meta={meta}
|
||||
tags={tags}
|
||||
post={post}
|
||||
totalPosts={allPosts}
|
||||
postCount={postCount}
|
||||
categories={categories}
|
||||
>
|
||||
<ArticleDetail post={post} blockMap={blockMap} allPosts={allPosts} prev={prev} next={next} />
|
||||
<ArticleDetail post={post} blockMap={blockMap} prev={prev} next={next} />
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export async function getStaticProps () {
|
||||
const next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
|
||||
return {
|
||||
props: { post, blockMap, tags, prev, next, allPosts, categories, postCount, latestPosts },
|
||||
props: { post, blockMap, tags, prev, next, categories, postCount, latestPosts },
|
||||
revalidate: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import ArticleDetail from '@/components/ArticleDetail'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import TocDrawer from '@/components/TocDrawer'
|
||||
import TocDrawerButton from '@/components/TocDrawerButton'
|
||||
import BaseLayout from '@/layouts/BaseLayout'
|
||||
import { getAllPosts, getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import Custom404 from '@/pages/404'
|
||||
import { getPageTableOfContents } from 'notion-utils'
|
||||
import { useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
@@ -16,7 +20,6 @@ const Slug = ({
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
allPosts,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
@@ -32,6 +35,10 @@ const Slug = ({
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
const drawerRight = useRef(null)
|
||||
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
|
||||
const floatSlot = post?.toc.length > 1 ? <div className="block lg:hidden"><TocDrawerButton onClick={() => { drawerRight?.current?.handleSwitchVisible() }} /></div> : null
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
meta={meta}
|
||||
@@ -39,8 +46,8 @@ const Slug = ({
|
||||
post={post}
|
||||
postCount={postCount}
|
||||
latestPosts={latestPosts}
|
||||
totalPosts={allPosts}
|
||||
categories={categories}
|
||||
floatSlot={floatSlot}
|
||||
>
|
||||
<ArticleDetail
|
||||
post={post}
|
||||
@@ -48,6 +55,15 @@ const Slug = ({
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
|
||||
{/* 悬浮目录按钮 */}
|
||||
<div className="block lg:hidden">
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
|
||||
{/* 宠物 */}
|
||||
<Live2D/>
|
||||
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
@@ -95,7 +111,6 @@ export async function getStaticProps ({ params: { slug } }) {
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
allPosts,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
|
||||
@@ -28,12 +28,11 @@ export async function getStaticProps () {
|
||||
)
|
||||
for (const i in postsToShow) {
|
||||
const post = postsToShow[i]
|
||||
const blockMap = await getPostBlocks(post.id, 'slug')
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', 12)
|
||||
if (blockMap) {
|
||||
post.blockMap = blockMap
|
||||
}
|
||||
}
|
||||
console.log('加载文章预览完成')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -54,12 +54,11 @@ export async function getStaticProps ({ params: { page } }) {
|
||||
|
||||
for (const i in postsToShow) {
|
||||
const post = postsToShow[i]
|
||||
const blockMap = await getPostBlocks(post.id, 'slug')
|
||||
const blockMap = await getPostBlocks(post.id, 'slug', 12)
|
||||
if (blockMap) {
|
||||
post.blockMap = blockMap
|
||||
}
|
||||
}
|
||||
console.log('加载文章预览完成')
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function getStaticProps () {
|
||||
}
|
||||
}
|
||||
|
||||
const Search = ({ posts, tags, categories, postCount, latestPosts }) => {
|
||||
const Search = ({ posts, tags, categories, postCount }) => {
|
||||
let filteredPosts = []
|
||||
const searchKey = getSearchKey()
|
||||
if (searchKey) {
|
||||
|
||||
@@ -7,14 +7,14 @@ import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
export default function Tag ({ tags, allPosts, categories, postCount, latestPosts }) {
|
||||
export default function Tag ({ tags, categories, postCount, latestPosts }) {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
|
||||
description: BLOG.description,
|
||||
type: 'website'
|
||||
}
|
||||
return <BaseLayout meta={meta} categories={categories} totalPosts={allPosts} postCount={postCount} latestPosts={latestPosts}>
|
||||
return <BaseLayout meta={meta} categories={categories} postCount={postCount} latestPosts={latestPosts}>
|
||||
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
|
||||
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTags} className='mr-4'/>{locale.COMMON.TAGS}:</div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
@@ -28,12 +28,11 @@ export default function Tag ({ tags, allPosts, categories, postCount, latestPost
|
||||
|
||||
export async function getStaticProps () {
|
||||
const from = 'tag-index-props'
|
||||
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
|
||||
const { categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
|
||||
|
||||
return {
|
||||
props: {
|
||||
tags,
|
||||
posts: allPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
|
||||
@@ -163,4 +163,29 @@ nav {
|
||||
|
||||
.medium-zoom-overlay{
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.article-cover{
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system,SF UI Text,Arial,PingFang SC,Hiragino Sans GB,Microsoft YaHei,WenQuanYi Micro Hei,sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: rgba(0,0,0,.75);
|
||||
font-variant-ligatures: common-ligatures;
|
||||
line-height: 1.625;
|
||||
tab-size: 4;
|
||||
outline: 0;
|
||||
font-weight: normal;
|
||||
-webkit-box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 998;
|
||||
padding-top: 160px;
|
||||
bottom: -1px;
|
||||
margin-top: -200px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,#fff 70%);
|
||||
padding-bottom: 34px;
|
||||
}
|
||||
Reference in New Issue
Block a user