Files
NotionNext/themes/next/index.js
2024-09-05 12:13:07 +08:00

432 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import replaceSearchResult from '@/components/Mark'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import Announcement from './components/Announcement'
import ArticleDetail from './components/ArticleDetail'
import { ArticleLock } from './components/ArticleLock'
import BlogListBar from './components/BlogListBar'
import BlogPostArchive from './components/BlogPostArchive'
import BlogPostListPage from './components/BlogPostListPage'
import BlogPostListScroll from './components/BlogPostListScroll'
import Card from './components/Card'
import FloatDarkModeButton from './components/FloatDarkModeButton'
import Footer from './components/Footer'
import JumpToBottomButton from './components/JumpToBottomButton'
import JumpToTopButton from './components/JumpToTopButton'
import SideAreaLeft from './components/SideAreaLeft'
import SideAreaRight from './components/SideAreaRight'
import StickyBar from './components/StickyBar'
import TagItem from './components/TagItem'
import TocDrawer from './components/TocDrawer'
import TocDrawerButton from './components/TocDrawerButton'
import TopNav from './components/TopNav'
import CONFIG from './config'
import { Style } from './style'
const AlgoliaSearchModal = dynamic(
() => import('@/components/AlgoliaSearchModal'),
{ ssr: false }
)
// 主题全局状态
const ThemeGlobalNext = createContext()
export const useNextGlobal = () => useContext(ThemeGlobalNext)
/**
* 基础布局 采用左中右三栏布局,移动端使用顶部导航栏
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = props => {
const { children, headerSlot, rightAreaSlot, post } = props
const targetRef = useRef(null)
const floatButtonGroup = useRef(null)
const [showRightFloat, 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 !== showRightFloat) {
switchShow(shouldShow)
}
changePercent(per)
}
useEffect(() => {
// facebook messenger 插件需要调整右下角悬浮按钮的高度
const fb = document.getElementsByClassName('fb-customerchat')
if (fb.length === 0) {
floatButtonGroup?.current?.classList.replace('bottom-24', 'bottom-12')
} else {
floatButtonGroup?.current?.classList.replace('bottom-12', 'bottom-24')
}
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [showRightFloat])
// 悬浮抽屉
const drawerRight = useRef(null)
const floatSlot = (
<div className='block lg:hidden'>
<TocDrawerButton
onClick={() => {
drawerRight?.current?.handleSwitchVisible()
}}
/>
</div>
)
const tocRef = isBrowser ? document.getElementById('article-wrapper') : null
const searchModal = useRef(null)
return (
<ThemeGlobalNext.Provider value={{ searchModal }}>
<div
id='theme-next'
className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}>
<Style />
{/* 移动端顶部导航栏 */}
<TopNav {...props} />
<AlgoliaSearchModal cRef={searchModal} {...props} />
<>{headerSlot}</>
{/* 顶部黑线装饰 */}
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block' />
{/* 主区 */}
<main
id='wrapper'
className={
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
? 'flex-row-reverse'
: '') + ' next relative flex justify-center flex-1 pb-12'
}>
{/* 左侧栏样式 */}
<SideAreaLeft targetRef={targetRef} {...props} />
{/* 中央内容 */}
<section
id='container-inner'
className={`${siteConfig('NEXT_NAV_TYPE', null, CONFIG) !== 'normal' ? 'mt-24' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full relative z-10`}
ref={targetRef}>
{children}
</section>
{/* 右侧栏样式 */}
{siteConfig('NEXT_RIGHT_BAR', null, CONFIG) && (
<SideAreaRight
targetRef={targetRef}
slot={rightAreaSlot}
{...props}
/>
)}
</main>
{/* 悬浮目录按钮 */}
{post && (
<div className='block lg:hidden'>
<TocDrawer post={post} cRef={drawerRight} targetRef={tocRef} />
</div>
)}
{/* 右下角悬浮 */}
<div
ref={floatButtonGroup}
className='right-8 bottom-12 lg:right-2 fixed justify-end z-20 '>
<div
className={
(showRightFloat ? 'animate__animated ' : 'hidden') +
' animate__fadeInUp rounded-md 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={siteConfig('TITLE')} />
</div>
</ThemeGlobalNext.Provider>
)
}
/**
* 首页
* 首页就是一个博客列表
* @param {*} props
* @returns
*/
const LayoutIndex = props => {
const { notice } = props
return (
<>
{/* 首页移动端顶部显示公告 */}
<Card className='my-2 lg:hidden'>
<Announcement post={notice} />
</Card>
<BlogListBar {...props} />
{siteConfig('POST_LIST_STYLE') !== 'page' ? (
<BlogPostListScroll {...props} showSummary={true} />
) : (
<BlogPostListPage {...props} />
)}
</>
)
}
/**
* 博客列表
* @param {*} props
* @returns
*/
const LayoutPostList = props => {
return (
<>
<BlogListBar {...props} />
{siteConfig('POST_LIST_STYLE') !== 'page' ? (
<BlogPostListScroll {...props} showSummary={true} />
) : (
<BlogPostListPage {...props} />
)}
</>
)
}
/**
* 搜索
* @param {*} props
* @returns
*/
const LayoutSearch = props => {
const { locale } = useGlobal()
const { posts, keyword } = props
useEffect(() => {
if (isBrowser) {
replaceSearchResult({
doms: document.getElementById('posts-wrapper'),
search: keyword,
target: {
element: 'span',
className: 'text-red-500 border-b border-dashed'
}
})
}
}, [])
return (
<>
<StickyBar>
<div className='p-4 dark:text-gray-200'>
<i className='mr-1 fas fa-search' /> {posts?.length}{' '}
{locale.COMMON.RESULT_OF_SEARCH}
</div>
</StickyBar>
<div className='md:mt-5'>
{siteConfig('POST_LIST_STYLE') !== 'page' ? (
<BlogPostListScroll {...props} showSummary={true} />
) : (
<BlogPostListPage {...props} />
)}
</div>
</>
)
}
/**
* 404
* @param {*} props
* @returns
*/
const Layout404 = props => {
const router = useRouter()
useEffect(() => {
// 延时3秒如果加载失败就返回首页
setTimeout(() => {
const article = isBrowser && document.getElementById('article-wrapper')
if (!article) {
router.push('/').then(() => {
// console.log('找不到页面', router.asPath)
})
}
}, 3000)
}, [])
return (
<>
<div className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
<div className='dark:text-gray-200'>
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'>
<i className='mr-2 fas fa-spinner animate-spin' />
404
</h2>
<div className='inline-block text-left h-32 leading-10 items-center'>
<h2 className='m-0 p-0'>页面无法加载即将返回首页</h2>
</div>
</div>
</div>
</>
)
}
/**
* 归档
* @param {*} props
* @returns
*/
const LayoutArchive = props => {
const { archivePosts } = props
return (
<>
<div className='mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-hexo-black-gray shadow-md min-h-full'>
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
</>
)
}
/**
* 文章详情
* @param {*} props
* @returns
*/
const LayoutSlug = props => {
const { post, lock, validPassword } = props
const router = useRouter()
useEffect(() => {
// 404
if (!post) {
setTimeout(
() => {
if (isBrowser) {
const article = document.getElementById('notion-article')
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
})
}
}
},
siteConfig('POST_WAITING_TIME_FOR_404') * 1000
)
}
}, [post])
return (
<>
{post && !lock && <ArticleDetail {...props} />}
{post && lock && <ArticleLock validPassword={validPassword} />}
</>
)
}
/**
* 分类列表
* @param {*} props
* @returns
*/
const LayoutCategoryIndex = props => {
const { allPosts, categoryOptions } = props
const { locale } = useGlobal()
return (
<div totalPosts={allPosts} {...props}>
<div className='bg-white dark:bg-hexo-black-gray px-10 py-10 shadow h-full'>
<div className='dark:text-gray-200 mb-5'>
<i className='mr-4 fas faTh' />
{locale.COMMON.CATEGORY}:
</div>
<div id='category-list' className='duration-200 flex flex-wrap'>
{categoryOptions?.map(category => {
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<div
className={
'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
}>
<i className='mr-4 fas fa-folder' />
{category.name}({category.count})
</div>
</Link>
)
})}
</div>
</div>
</div>
)
}
/**
* 标签列表
* @param {*} props
* @returns
*/
const LayoutTagIndex = props => {
const { tagOptions } = props
const { locale } = useGlobal()
return (
<>
<div className='bg-white dark:bg-hexo-black-gray px-10 py-10 shadow h-full'>
<div className='dark:text-gray-200 mb-5'>
<i className='fas fa-tags mr-4' />
{locale.COMMON.TAGS}:
</div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{tagOptions.map(tag => {
return (
<div key={tag.name} className='p-2'>
<TagItem key={tag.name} tag={tag} />
</div>
)
})}
</div>
</div>
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
}