mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-13 23:16:47 +00:00
theme-matery
This commit is contained in:
@@ -36,14 +36,14 @@ function initLive2D() {
|
||||
window.removeEventListener('scroll', initLive2D)
|
||||
setTimeout(() => {
|
||||
// 加载 waifu.css live2d.min.js waifu-tips.js
|
||||
if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
// loadExternalResource('https://cdn.zhangxinxu.com/sp/demo/live2d/live2d/js/live2d.js', 'js')
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
})
|
||||
}
|
||||
// if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
// loadExternalResource('https://cdn.zhangxinxu.com/sp/demo/live2d/live2d/js/live2d.js', 'js')
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
})
|
||||
// }
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ import React from 'react'
|
||||
const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
|
||||
const router = useRouter()
|
||||
React.useEffect(() => {
|
||||
// 页面渲染后删除hidden属性
|
||||
// const sideBarWrapperElement = document.getElementById('sidebar-wrapper')
|
||||
// sideBarWrapperElement?.classList?.remove('hidden')
|
||||
|
||||
const sideBarDrawerRouteListener = () => {
|
||||
switchSideDrawerVisible(false)
|
||||
}
|
||||
@@ -25,29 +21,30 @@ const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
|
||||
// 点击按钮更改侧边抽屉状态
|
||||
const switchSideDrawerVisible = (showStatus) => {
|
||||
if (showStatus) {
|
||||
onOpen()
|
||||
onOpen && onOpen()
|
||||
} else {
|
||||
onClose()
|
||||
onClose && onClose()
|
||||
}
|
||||
const sideBarDrawer = window.document.getElementById('sidebar-drawer')
|
||||
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
|
||||
|
||||
if (showStatus) {
|
||||
sideBarDrawer.classList.replace('-ml-80', 'ml-0')
|
||||
sideBarDrawer.classList.replace('-ml-56', 'ml-0')
|
||||
sideBarDrawerBackground.classList.replace('hidden', 'block')
|
||||
} else {
|
||||
sideBarDrawer.classList.replace('ml-0', '-ml-80')
|
||||
sideBarDrawer.classList.replace('ml-0', '-ml-56')
|
||||
sideBarDrawerBackground.classList.replace('block', 'hidden')
|
||||
}
|
||||
}
|
||||
|
||||
return <div id='sidebar-wrapper' className={' ' + className}>
|
||||
<div id='sidebar-drawer' className={`${isOpen ? 'ml-0' : '-ml-80'} bg-white dark:bg-gray-900 flex flex-col duration-300 fixed h-full left-0 overflow-y-scroll scroll-hidden top-0 z-40`}>
|
||||
return <div id='sidebar-wrapper' className={' ' + className }>
|
||||
<div id='sidebar-drawer' className={`${isOpen ? 'ml-0 w-56' : '-ml-56'} bg-white dark:bg-gray-900 shadow-black shadow-lg flex flex-col duration-300 fixed h-full left-0 overflow-y-scroll scroll-hidden top-0 z-30`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 背景蒙版 */}
|
||||
<div id='sidebar-drawer-background' onClick={() => { switchSideDrawerVisible(false) }}
|
||||
className={`${isOpen ? 'block' : 'hidden'} animate__animated animate__fadeIn fixed top-0 duration-300 left-0 z-30 w-full h-full glassmorphism`}/>
|
||||
className={`${isOpen ? 'block' : 'hidden'} animate__animated animate__fadeIn fixed top-0 duration-300 left-0 z-20 w-full h-full bg-black/70`}/>
|
||||
</div>
|
||||
}
|
||||
export default SideBarDrawer
|
||||
|
||||
@@ -8,7 +8,7 @@ import NotionPage from '@/components/NotionPage'
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<div className="w-full shadow-sm hover:shadow border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray duration-300">
|
||||
<div className="w-full drop-shadow-md hover:shadow border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray duration-300">
|
||||
<div
|
||||
key={post.id}
|
||||
className="animate__animated animate__fadeIn flex flex-col-reverse lg:flex-row justify-between duration-300"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow hover:shadow dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
|
||||
<section className=" drop-shadow-md hover:shadow-md dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ const TopNav = props => {
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'top-0 shadow-md fixed bg-none animate__animated animate__fadeIn dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full z-20 transform duration-200 border-transparent dark:border-transparent'}>
|
||||
<div id='sticky-nav' className={'top-0 drop-shadow-md fixed bg-none animate__animated animate__fadeIn dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full z-20 transform duration-200 border-transparent dark:border-transparent'}>
|
||||
<div className='w-full flex justify-between items-center px-4 py-2'>
|
||||
<div className='flex'>
|
||||
<Logo {...props}/>
|
||||
|
||||
@@ -6,7 +6,8 @@ import * as fukasawa from './fukasawa'
|
||||
import * as hexo from './hexo'
|
||||
import * as medium from './medium'
|
||||
import * as nobelium from './nobelium'
|
||||
import * as matery from './matery'
|
||||
import * as example from './example'
|
||||
|
||||
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'nobelium', 'example']
|
||||
export { hexo, next, medium, fukasawa, nobelium, example }
|
||||
export const ALL_THEME = ['hexo', 'next', 'medium', 'fukasawa', 'nobelium', 'matery', 'example']
|
||||
export { hexo, next, medium, fukasawa, nobelium, matery, example }
|
||||
|
||||
32
themes/matery/Layout404.js
Normal file
32
themes/matery/Layout404.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const Layout404 = props => {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
// 延时3秒如果加载失败就返回首页
|
||||
setTimeout(() => {
|
||||
const article = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/').then(() => {
|
||||
// console.log('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<div className="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">
|
||||
404
|
||||
</h2>
|
||||
<div className="inline-block text-left h-32 leading-10 items-center">
|
||||
<h2 className="m-0 p-0">页面未找到</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
53
themes/matery/LayoutArchive.js
Normal file
53
themes/matery/LayoutArchive.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useEffect } from 'react'
|
||||
import BlogPostArchive from './components/BlogPostArchive'
|
||||
import Card from './components/Card'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutArchive = (props) => {
|
||||
const { posts } = props
|
||||
// 深拷贝
|
||||
const postsSortByDate = Object.create(posts)
|
||||
|
||||
// 时间排序
|
||||
postsSortByDate.sort((a, b) => {
|
||||
const dateA = new Date(a?.date?.start_date || a.createdTime)
|
||||
const dateB = new Date(b?.date?.start_date || b.createdTime)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const archivePosts = {}
|
||||
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = post.date?.start_date.slice(0, 7)
|
||||
if (archivePosts[date]) {
|
||||
archivePosts[date].push(post)
|
||||
} else {
|
||||
archivePosts[date] = [post]
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const anchor = window.location.hash
|
||||
if (anchor) {
|
||||
setTimeout(() => {
|
||||
const anchorElement = document.getElementById(anchor.substring(1))
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, [])
|
||||
return <LayoutBase {...props} >
|
||||
<Card className='w-full'>
|
||||
<div className="mb-10 pb-20 bg-white md:p-12 p-3 min-h-full dark:bg-hexo-black-gray">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogPostArchive
|
||||
key={archiveTitle}
|
||||
posts={archivePosts[archiveTitle]}
|
||||
archiveTitle={archiveTitle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</LayoutBase>
|
||||
}
|
||||
87
themes/matery/LayoutBase.js
Normal file
87
themes/matery/LayoutBase.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import Footer from './components/Footer'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
import TopNav from './components/TopNav'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import LoadingCover from './components/LoadingCover'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param props
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, headerSlot, meta, siteInfo } = props
|
||||
const [show, switchShow] = useState(false)
|
||||
const { onLoading } = useGlobal()
|
||||
|
||||
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 className="bg-hexo-background-gray dark:bg-black w-full overflow-hidden">
|
||||
|
||||
<CommonHead meta={meta} siteInfo={siteInfo}/>
|
||||
|
||||
<TopNav {...props} />
|
||||
|
||||
{headerSlot}
|
||||
|
||||
<main id="wrapper" className="w-full py-8 md:px-8 lg:px-24 min-h-screen">
|
||||
<div
|
||||
id="container-inner"
|
||||
className="pt-14 w-full mx-auto lg:flex lg:space-x-4 justify-center"
|
||||
>
|
||||
<div className="w-full max-w-4xl overflow-x-hidden">
|
||||
{onLoading ? <LoadingCover /> : children}
|
||||
</div>
|
||||
{/* <SideRight {...props} slot={rightAreaSlot} /> */}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 左下角悬浮 */}
|
||||
<div className="bottom-4 -left-14 fixed justify-end z-40">
|
||||
<Live2D />
|
||||
</div>
|
||||
|
||||
{/* 右下角悬浮 */}
|
||||
<div className="bottom-12 right-2 fixed justify-end z-20">
|
||||
<div
|
||||
className={
|
||||
(show ? 'animate__animated ' : 'hidden') +
|
||||
' animate__fadeInUp justify-center duration-300 animate__faster flex flex-col items-center cursor-pointer '
|
||||
}
|
||||
>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer title={siteInfo?.title || BLOG.TITLE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
15
themes/matery/LayoutCategory.js
Normal file
15
themes/matery/LayoutCategory.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export const LayoutCategory = props => {
|
||||
const { category } = props
|
||||
return <LayoutBase {...props}>
|
||||
<div className="cursor-pointer text-lg px-5 py-1 mb-2 font-light hover:text-indigo-700 dark:hover:text-indigo-400 transform dark:text-white">
|
||||
<i className="mr-1 far fa-folder-open" />
|
||||
{category}
|
||||
</div>
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
|
||||
</LayoutBase>
|
||||
}
|
||||
35
themes/matery/LayoutCategoryIndex.js
Normal file
35
themes/matery/LayoutCategoryIndex.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import Card from './components/Card'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategoryIndex = props => {
|
||||
const { categories } = props
|
||||
const { locale } = useGlobal()
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<Card className="w-full min-h-screen">
|
||||
<div className="dark:text-gray-200 mb-5 mx-3">
|
||||
<i className="mr-4 fas fa-th" />
|
||||
{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id="category-list" className="duration-200 flex flex-wrap mx-8">
|
||||
{categories.map(category => {
|
||||
return (
|
||||
<Link key={category.name} href={`/category/${category.name}`} passHref>
|
||||
<div
|
||||
className={
|
||||
' duration-300 dark:hover:text-white px-5 cursor-pointer py-2 hover:text-indigo-400'
|
||||
}
|
||||
>
|
||||
<i className="mr-4 fas fa-folder" />
|
||||
{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
13
themes/matery/LayoutIndex.js
Normal file
13
themes/matery/LayoutIndex.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import Header from './components/Header'
|
||||
import CONFIG_MATERY from './config_matery'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import React from 'react'
|
||||
|
||||
export const LayoutIndex = (props) => {
|
||||
return <LayoutBase {...props} headerSlot={CONFIG_MATERY.HOME_BANNER_ENABLE && <Header {...props} />}>
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
|
||||
</LayoutBase>
|
||||
}
|
||||
9
themes/matery/LayoutPage.js
Normal file
9
themes/matery/LayoutPage.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutPage = (props) => {
|
||||
const { page, posts, postCount } = props
|
||||
return <LayoutBase {...props}>
|
||||
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
|
||||
</LayoutBase>
|
||||
}
|
||||
99
themes/matery/LayoutSearch.js
Normal file
99
themes/matery/LayoutSearch.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import SearchInput from './components/SearchInput'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Mark from 'mark.js'
|
||||
import TagItemMini from './components/TagItemMini'
|
||||
import Card from './components/Card'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const LayoutSearch = props => {
|
||||
const { keyword, tags, categories } = props
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const currentSearch = keyword || router?.query?.s
|
||||
const cRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
// 自动聚焦到搜索框
|
||||
cRef?.current?.focus()
|
||||
if (currentSearch) {
|
||||
const targets = document.getElementsByClassName('replace')
|
||||
for (const container of targets) {
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(currentSearch, 'gim')
|
||||
const instance = new Mark(container)
|
||||
instance.markRegExp(re, {
|
||||
element: 'span',
|
||||
className: 'text-red-500 border-b border-dashed'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
return (
|
||||
<LayoutBase {...props} currentSearch={currentSearch}>
|
||||
{!currentSearch && <>
|
||||
<div className="my-6 px-2">
|
||||
<SearchInput cRef={cRef} {...props} />
|
||||
{/* 分类 */}
|
||||
<Card className="w-full mt-4">
|
||||
<div className="dark:text-gray-200 mb-5 mx-3">
|
||||
<i className="mr-4 fas fa-th" />
|
||||
{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div id="category-list" className="duration-200 flex flex-wrap mx-8">
|
||||
{categories?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
>
|
||||
<div
|
||||
className={
|
||||
' duration-300 dark:hover:text-white rounded-lg px-5 cursor-pointer py-2 hover:bg-indigo-400 hover:text-white'
|
||||
}
|
||||
>
|
||||
<i className="mr-4 fas fa-folder" />
|
||||
{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
{/* 标签 */}
|
||||
<Card className="w-full mt-4">
|
||||
<div className="dark:text-gray-200 mb-5 ml-4">
|
||||
<i className="mr-4 fas fa-tag" />
|
||||
{locale.COMMON.TAGS}:
|
||||
</div>
|
||||
<div id="tags-list" className="duration-200 flex flex-wrap ml-8">
|
||||
{tags?.map(tag => {
|
||||
return (
|
||||
<div key={tag.name} className="p-2">
|
||||
<TagItemMini key={tag.name} tag={tag} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{currentSearch && <>
|
||||
<div id="container">
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
|
||||
</div>
|
||||
</>}
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
92
themes/matery/LayoutSlug.js
Normal file
92
themes/matery/LayoutSlug.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useRef } from 'react'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import HeaderArticle from './components/HeaderArticle'
|
||||
import JumpToCommentButton from './components/JumpToCommentButton'
|
||||
import TocDrawer from './components/TocDrawer'
|
||||
import TocDrawerButton from './components/TocDrawerButton'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import Comment from '@/components/Comment'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import ArticleAdjacent from './components/ArticleAdjacent'
|
||||
import ArticleCopyright from './components/ArticleCopyright'
|
||||
import ArticleRecommend from './components/ArticleRecommend'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
|
||||
export const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
const drawerRight = useRef(null)
|
||||
|
||||
if (!post) {
|
||||
return <LayoutBase
|
||||
headerSlot={<HeaderArticle {...props} />}
|
||||
{...props}
|
||||
showCategory={false}
|
||||
showTag={false}
|
||||
></LayoutBase>
|
||||
}
|
||||
|
||||
const targetRef = isBrowser() ? document.getElementById('container') : null
|
||||
|
||||
const floatSlot = <>
|
||||
{post?.toc?.length > 1 && <div className="block lg:hidden">
|
||||
<TocDrawerButton
|
||||
onClick={() => {
|
||||
drawerRight?.current?.handleSwitchVisible()
|
||||
}}
|
||||
/>
|
||||
</div>}
|
||||
<JumpToCommentButton />
|
||||
</>
|
||||
|
||||
return (
|
||||
<LayoutBase
|
||||
headerSlot={<HeaderArticle {...props} />}
|
||||
{...props}
|
||||
showCategory={false}
|
||||
showTag={false}
|
||||
floatSlot={floatSlot}
|
||||
>
|
||||
<div className="w-full lg:shadow-sm lg:hover:shadow lg:border lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{!lock && <div id="container" className="overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">
|
||||
|
||||
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased" >
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-5 justify-center mx-auto max-w-2xl lg:max-w-full'>
|
||||
{post && <NotionPage post={post} />}
|
||||
</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" />
|
||||
</section>
|
||||
|
||||
{post.type === 'Post' && <ArticleCopyright {...props} /> }
|
||||
{post.type === 'Post' && <ArticleRecommend {...props} /> }
|
||||
{post.type === 'Post' && <ArticleAdjacent {...props} /> }
|
||||
|
||||
</article>
|
||||
|
||||
<hr className='border-dashed' />
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="duration-200 overflow-x-auto bg-white dark:bg-hexo-black-gray px-3">
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div className='block lg:hidden'>
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
26
themes/matery/LayoutTag.js
Normal file
26
themes/matery/LayoutTag.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListScroll from './components/BlogPostListScroll'
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const LayoutTag = (props) => {
|
||||
const tag = props.tags.find((t) => {
|
||||
return t.name === props.tag
|
||||
})
|
||||
|
||||
return <LayoutBase {...props}>
|
||||
{tag && (
|
||||
<div className="cursor-pointer px-3 py-2 mb-2 font-light hover:text-indigo-700 dark:hover:text-indigo-400 transform dark:text-white">
|
||||
<Link key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded duration-200
|
||||
mr-2 py-0.5 px-1 text-xl whitespace-nowrap ` }>
|
||||
<div className='font-light dark:text-gray-400 dark:hover:text-white'> #{tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
|
||||
</LayoutBase>
|
||||
}
|
||||
28
themes/matery/LayoutTagIndex.js
Normal file
28
themes/matery/LayoutTagIndex.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Card from './components/Card'
|
||||
import TagItemMini from './components/TagItemMini'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTagIndex = props => {
|
||||
const { tags } = props
|
||||
const { locale } = useGlobal()
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<Card className='w-full'>
|
||||
<div className="dark:text-gray-200 mb-5 ml-4">
|
||||
<i className="mr-4 fas fa-tag" />
|
||||
{locale.COMMON.TAGS}:
|
||||
</div>
|
||||
<div id="tags-list" className="duration-200 flex flex-wrap ml-8">
|
||||
{tags.map(tag => {
|
||||
return (
|
||||
<div key={tag.name} className="p-2">
|
||||
<TagItemMini key={tag.name} tag={tag} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
30
themes/matery/components/AnalyticsCard.js
Normal file
30
themes/matery/components/AnalyticsCard.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Card from './Card'
|
||||
|
||||
export function AnalyticsCard (props) {
|
||||
const { postCount } = props
|
||||
return <Card>
|
||||
<div className='ml-2 mb-3 '>
|
||||
<i className='fas fa-chart-area' /> 统计
|
||||
</div>
|
||||
<div className='text-xs font-light justify-center mx-7'>
|
||||
<div className='inline'>
|
||||
<div className='flex justify-between'>
|
||||
<div>文章数:</div>
|
||||
<div>{postCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden busuanzi_container_page_pv ml-2'>
|
||||
<div className='flex justify-between'>
|
||||
<div>访问量:</div>
|
||||
<div className='busuanzi_value_page_pv' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden busuanzi_container_site_uv ml-2'>
|
||||
<div className='flex justify-between'>
|
||||
<div>访客数:</div>
|
||||
<div className='busuanzi_value_site_uv' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
25
themes/matery/components/ArticleAdjacent.js
Normal file
25
themes/matery/components/ArticleAdjacent.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Link from 'next/link'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
/**
|
||||
* 上一篇,下一篇文章
|
||||
* @param {prev,next} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleAdjacent ({ prev, next }) {
|
||||
if (!prev || !next || !CONFIG_MATERY.ARTICLE_ADJACENT) {
|
||||
return <></>
|
||||
}
|
||||
return <section className='text-gray-800 items-center text-xs md:text-sm flex justify-between m-1 '>
|
||||
<Link href={`/${prev.slug}`} passHref>
|
||||
<a className='py-1 cursor-pointer hover:underline justify-start items-center dark:text-white flex w-full h-full duration-200'>
|
||||
<i className='mr-1 fas fa-angle-left' />{prev.title}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/${next.slug}`} passHref>
|
||||
<a className='py-1 cursor-pointer hover:underline justify-end items-center dark:text-white flex w-full h-full duration-200'>{next.title}
|
||||
<i className='ml-1 my-1 fas fa-angle-right' />
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
}
|
||||
39
themes/matery/components/ArticleCopyright.js
Normal file
39
themes/matery/components/ArticleCopyright.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
export default function ArticleCopyright () {
|
||||
if (!CONFIG_MATERY.ARTICLE_COPYRIGHT) {
|
||||
return <></>
|
||||
}
|
||||
const router = useRouter()
|
||||
const [path, setPath] = useState(BLOG.LINK + router.asPath)
|
||||
useEffect(() => {
|
||||
setPath(window.location.href)
|
||||
})
|
||||
|
||||
const { locale } = useGlobal()
|
||||
return <section className="dark:text-gray-300 mt-6 mx-1 ">
|
||||
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-900 bg-gray-100 p-5 leading-8 border-l-2 border-indigo-500">
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
|
||||
<Link href={'/about'} >
|
||||
<a className="hover:underline">{BLOG.AUTHOR}</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.URL}:</strong>
|
||||
<a className="hover:underline" href={path}>
|
||||
{path}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
|
||||
{locale.COMMON.COPYRIGHT_NOTICE}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
37
themes/matery/components/ArticleLock.js
Normal file
37
themes/matery/components/ArticleLock.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleLock = props => {
|
||||
const { validPassword } = props
|
||||
const { locale } = useGlobal()
|
||||
const submitPassword = () => {
|
||||
const p = document.getElementById('password')
|
||||
if (!validPassword(p?.value)) {
|
||||
const tips = document.getElementById('tips')
|
||||
if (tips) {
|
||||
tips.innerHTML = ''
|
||||
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 '>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 bg-indigo-500 hover:bg-indigo-400 text-white rounded-r duration-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
66
themes/matery/components/ArticleRecommend.js
Normal file
66
themes/matery/components/ArticleRecommend.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import Link from 'next/link'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 关联推荐文章
|
||||
* @param {prev,next} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleRecommend({ recommendPosts, siteInfo }) {
|
||||
if (
|
||||
!CONFIG_MATERY.ARTICLE_RECOMMEND ||
|
||||
!recommendPosts ||
|
||||
recommendPosts.length === 0
|
||||
) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
|
||||
<div>
|
||||
<i className="mr-2 fas fa-thumbs-up" />
|
||||
{locale.COMMON.RELATE_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{recommendPosts.map(post => {
|
||||
const headerImage = post?.page_cover
|
||||
? `url("${post.page_cover}")`
|
||||
: `url("${siteInfo?.pageCover}")`
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
key={post.id}
|
||||
className="flex h-40 cursor-pointer overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center bg-no-repeat hover:scale-110 transform duration-200"
|
||||
style={{ backgroundImage: headerImage }}
|
||||
>
|
||||
<div className="flex items-center justify-center bg-black bg-opacity-60 hover:bg-opacity-10 w-full h-full duration-300 ">
|
||||
<div className=" text-sm text-white text-center shadow-text">
|
||||
<div>
|
||||
<i className="fas fa-calendar-alt mr-1" />
|
||||
{post.date?.start_date}
|
||||
</div>
|
||||
<div className="hover:underline">{post.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
themes/matery/components/BlogPostArchive.js
Normal file
46
themes/matery/components/BlogPostArchive.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
* 博客归档列表
|
||||
* @param posts 所有文章
|
||||
* @param archiveTitle 归档标题
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="pt-16 pb-4 text-3xl dark:text-gray-300"
|
||||
id={archiveTitle}
|
||||
>
|
||||
{archiveTitle}
|
||||
</div>
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
<li
|
||||
key={post.id}
|
||||
className="border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-indigo-500 dark:hover:border-indigo-300 dark:border-indigo-400 transform duration-500"
|
||||
>
|
||||
<div id={post?.date?.start_date}>
|
||||
<span className="text-gray-400">{post.date?.start_date}</span>{' '}
|
||||
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref>
|
||||
<a className="dark:text-gray-400 dark:hover:text-indigo-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
{post.title}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPostArchive
|
||||
93
themes/matery/components/BlogPostCard.js
Normal file
93
themes/matery/components/BlogPostCard.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const showPreview = CONFIG_MATERY.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<div className="w-full drop-shadow-md hover:shadow border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray duration-500 hover:scale-105">
|
||||
<div key={post.id} className="flex flex-col justify-between ">
|
||||
|
||||
{CONFIG_MATERY.POST_LIST_COVER && !showPreview && post?.page_cover && !post.results && (
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref>
|
||||
<div className="flex w-full relative duration-200 bg-black rounded-t-md lg:rounded-r-md lg:rounded-t-none cursor-pointer transform overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
className="opacity-50 max-h-52 lg:max-h-72 w-full hover:scale-125 rounded-t-md lg:rounded-r-md lg:rounded-t-none transform object-cover duration-500"
|
||||
/>
|
||||
<span className='absolute bottom-0 left-0 text-white p-6 text-2xl' > {post.title}</span>
|
||||
|
||||
{/* <Image className='hover:scale-125 rounded-t-xl lg:rounded-r-xl lg:rounded-t-none transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' /> */}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="lg:p-8 p-4 flex flex-col w-full">
|
||||
|
||||
{(!showPreview || showSummary) && !post.results && post.summary && (
|
||||
<p style={{ overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: '4', WebkitBoxOrient: 'vertical' }}
|
||||
className="replace h-full max-h-32 my-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 搜索结果 */}
|
||||
{post.results && (
|
||||
<p className="mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.results.map(r => (
|
||||
<span key={r}>{r}</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showPreview && (
|
||||
<div className="overflow-ellipsis truncate">
|
||||
<NotionPage post={post} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='text-gray-800 justify-between flex'>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="font-light hover:underline cursor-pointer text-sm leading-4 mr-3">
|
||||
<i className="far fa-clock mr-1" />
|
||||
{post.date?.start_date || post.lastEditedTime}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer font-light text-sm hover:underline hover:text-indigo-700 dark:hover:text-indigo-400 transform">
|
||||
<i className="mr-1 far fa-folder" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post?.tagItems && post?.tagItems.length > 0 && (<>
|
||||
<hr />
|
||||
<div className="text-gray-400 justify-between flex px-5 py-3">
|
||||
<div className="md:flex-nowrap flex-wrap md:justify-start inline-block">
|
||||
<div>
|
||||
{' '}
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItemMini key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostCard
|
||||
14
themes/matery/components/BlogPostListEmpty.js
Normal file
14
themes/matery/components/BlogPostListEmpty.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 空白博客 列表
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListEmpty = ({ currentSearch }) => {
|
||||
const { locale } = useGlobal()
|
||||
return <div className='flex w-full items-center justify-center min-h-screen mx-auto md:-mt-20'>
|
||||
<div className='text-gray-500 dark:text-gray-300'>{locale.COMMON.NO_MORE} {(currentSearch && <div>{currentSearch}</div>)}</div>
|
||||
</div>
|
||||
}
|
||||
export default BlogPostListEmpty
|
||||
35
themes/matery/components/BlogPostListPage.js
Normal file
35
themes/matery/components/BlogPostListPage.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import PaginationNumber from './PaginationNumber'
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import PaginationSimple from './PaginationSimple'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
* @param page 当前页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
const showPagination = postCount >= BLOG.POSTS_PER_PAGE
|
||||
if (!posts || posts.length === 0 || page > totalPage) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container" className='w-full'>
|
||||
{/* 文章列表 */}
|
||||
<div className="space-y-6 px-4 pt-4">
|
||||
{posts.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
{showPagination && <PaginationSimple page={page} totalPage={totalPage} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPostListPage
|
||||
74
themes/matery/components/BlogPostListScroll.js
Normal file
74
themes/matery/components/BlogPostListScroll.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import { getListByPage } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 博客列表滚动分页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MATERY.POST_LIST_SUMMARY }) => {
|
||||
const postsPerPage = BLOG.POSTS_PER_PAGE
|
||||
const [page, updatePage] = React.useState(1)
|
||||
const postsToShow = getListByPage(posts, page, postsPerPage)
|
||||
|
||||
let hasMore = false
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * postsPerPage < totalCount
|
||||
}
|
||||
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = React.useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
|
||||
// 监听滚动
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
})
|
||||
|
||||
const targetRef = React.useRef(null)
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!postsToShow || postsToShow.length === 0) {
|
||||
return <BlogPostListEmpty currentSearch={currentSearch} />
|
||||
} else {
|
||||
return <div id='container' ref={targetRef} className='w-full'>
|
||||
|
||||
{/* 文章列表 */}
|
||||
<div className='flex flex-wrap space-y-1 lg:space-y-4 px-2'>
|
||||
{postsToShow.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} showSummary={showSummary}/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div onClick={() => { handleGetMore() }}
|
||||
className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200'
|
||||
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`} </div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPostListScroll
|
||||
9
themes/matery/components/Card.js
Normal file
9
themes/matery/components/Card.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow hover:shadow dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
export default Card
|
||||
91
themes/matery/components/Catalog.js
Normal file
91
themes/matery/components/Catalog.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useRef } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { uuidToId } from 'notion-utils'
|
||||
import Progress from './Progress'
|
||||
|
||||
/**
|
||||
* 目录导航组件
|
||||
* @param toc
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Catalog = ({ toc }) => {
|
||||
// 无目录就直接返回空
|
||||
if (!toc || toc.length < 1) {
|
||||
return <></>
|
||||
}
|
||||
// 监听滚动事件
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', actionSectionScrollSpy)
|
||||
actionSectionScrollSpy()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', actionSectionScrollSpy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 目录自动滚动
|
||||
const tRef = useRef(null)
|
||||
const tocIds = []
|
||||
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = React.useState(null)
|
||||
const throttleMs = 100
|
||||
const actionSectionScrollSpy = React.useCallback(throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
let currentSectionId = activeSection
|
||||
for (let i = 0; i < sections.length; ++i) {
|
||||
const section = sections[i]
|
||||
if (!section || !(section instanceof Element)) continue
|
||||
if (!currentSectionId) {
|
||||
currentSectionId = section.getAttribute('data-id')
|
||||
}
|
||||
const bbox = section.getBoundingClientRect()
|
||||
const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
|
||||
const offset = Math.max(150, prevHeight / 4)
|
||||
// GetBoundingClientRect returns values relative to viewport
|
||||
if (bbox.top - offset < 0) {
|
||||
currentSectionId = section.getAttribute('data-id')
|
||||
prevBBox = bbox
|
||||
continue
|
||||
}
|
||||
// No need to continue loop, if last element has been detected
|
||||
break
|
||||
}
|
||||
setActiveSection(currentSectionId)
|
||||
const index = tocIds.indexOf(currentSectionId) || 0
|
||||
tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
|
||||
}, throttleMs))
|
||||
|
||||
return <div className='px-3'>
|
||||
<div className='w-full'><i className='mr-1 fas fa-stream' /> 目录</div>
|
||||
<div className='w-full py-3'>
|
||||
<Progress />
|
||||
</div>
|
||||
<div className='overflow-y-auto max-h-36 lg:max-h-96 overscroll-none scroll-hidden' ref={tRef}>
|
||||
<nav className='h-full text-black'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
tocIds.push(id)
|
||||
return (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-200
|
||||
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
|
||||
>
|
||||
<span style={{ display: 'inline-block', marginLeft: tocItem.indentLevel * 16 }}
|
||||
className={`${activeSection === id && ' font-bold text-indigo-400 underline'}`}
|
||||
>
|
||||
{tocItem.text}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Catalog
|
||||
25
themes/matery/components/CategoryGroup.js
Normal file
25
themes/matery/components/CategoryGroup.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
const CategoryGroup = ({ currentCategory, categories }) => {
|
||||
if (!categories) {
|
||||
return <></>
|
||||
}
|
||||
return <>
|
||||
<div id='category-list' className='dark:border-gray-600 flex flex-wrap mx-4'>
|
||||
{categories.map(category => {
|
||||
const selected = currentCategory === category.name
|
||||
return <Link key={category.name} href={`/category/${category.name}`} passHref>
|
||||
<a className={(selected
|
||||
? 'hover:text-white dark:hover:text-white bg-indigo-600 text-white '
|
||||
: 'dark:text-gray-400 text-gray-500 hover:text-white dark:hover:text-white hover:bg-indigo-600') +
|
||||
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
|
||||
<div> <i className={`mr-2 fas ${selected ? 'fa-folder-open' : 'fa-folder'}`} />{category.name}({category.count})</div>
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default CategoryGroup
|
||||
75
themes/matery/components/Collapse.js
Normal file
75
themes/matery/components/Collapse.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 折叠面板组件,支持水平折叠、垂直折叠
|
||||
* @param {type:['horizontal','vertical'],isOpen} props
|
||||
* @returns
|
||||
*/
|
||||
const Collapse = props => {
|
||||
const collapseRef = React.useRef(null)
|
||||
const type = props.type || 'vertical'
|
||||
const collapseSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
const sectionWidth = element.scrollWidth
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
switch (type) {
|
||||
case 'horizontal':
|
||||
element.style.width = sectionWidth + 'px'
|
||||
requestAnimationFrame(function () {
|
||||
element.style.width = 0 + 'px'
|
||||
})
|
||||
break
|
||||
case 'vertical':
|
||||
element.style.height = sectionHeight + 'px'
|
||||
requestAnimationFrame(function () {
|
||||
element.style.height = 0 + 'px'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开
|
||||
* @param {*} element
|
||||
*/
|
||||
const expandSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
const sectionWidth = element.scrollWidth
|
||||
let clearTime = 0
|
||||
switch (type) {
|
||||
case 'horizontal':
|
||||
element.style.width = sectionWidth + 'px'
|
||||
clearTime = setTimeout(() => {
|
||||
element.style.width = 'auto'
|
||||
}, 400)
|
||||
break
|
||||
case 'vertical':
|
||||
element.style.height = sectionHeight + 'px'
|
||||
clearTime = setTimeout(() => {
|
||||
element.style.height = 'auto'
|
||||
}, 400)
|
||||
}
|
||||
|
||||
clearTimeout(clearTime)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = collapseRef.current
|
||||
if (props.isOpen) {
|
||||
expandSection(element)
|
||||
} else {
|
||||
collapseSection(element)
|
||||
}
|
||||
}, [props.isOpen])
|
||||
|
||||
return (
|
||||
<div ref={collapseRef} style={type === 'vertical' ? { height: '0px' } : { width: '0px' }}
|
||||
className={'overflow-hidden duration-200 fixed z-50 ' + props.className }>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Collapse.defaultProps = { isOpen: false }
|
||||
|
||||
export default Collapse
|
||||
30
themes/matery/components/FloatDarkModeButton.js
Normal file
30
themes/matery/components/FloatDarkModeButton.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies } from '@/lib/theme'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
export default function FloatDarkModeButton () {
|
||||
if (!CONFIG_MATERY.WIDGET_DARK_MODE) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const { isDarkMode, updateDarkMode } = useGlobal()
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newStatus = !isDarkMode
|
||||
saveDarkModeToCookies(newStatus)
|
||||
updateDarkMode(newStatus)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleChangeDarkMode}
|
||||
className={'justify-center items-center w-7 h-7 text-center transform hover:scale-105 duration-200'
|
||||
}
|
||||
>
|
||||
<i id="darkModeButton" className={`${isDarkMode ? 'fa-sun' : 'fa-moon'} fas text-xs`}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
37
themes/matery/components/Footer.js
Normal file
37
themes/matery/components/Footer.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
|
||||
const Footer = ({ title }) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const copyrightDate = (function() {
|
||||
if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
|
||||
return BLOG.SINCE + '-' + currentYear
|
||||
}
|
||||
return currentYear
|
||||
})()
|
||||
|
||||
return (
|
||||
<footer
|
||||
className=' dark:bg-black flex-shrink-0 bg-indigo-700 text-gray-300 justify-center text-center m-auto w-full leading-6 dark:text-gray-100 text-sm p-6'
|
||||
>
|
||||
<DarkModeButton/>
|
||||
|
||||
<i className='fas fa-copyright' /> {`${copyrightDate}`} <span><i className='mx-1 animate-pulse fas fa-heart'/> <a href={BLOG.LINK} className='underline font-bold dark:text-gray-300 '>{BLOG.AUTHOR}</a>.<br/>
|
||||
|
||||
{BLOG.BEI_AN && <><i className='fas fa-shield-alt' /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
|
||||
|
||||
<span className='hidden busuanzi_container_site_pv'>
|
||||
<i className='fas fa-eye'/><span className='px-1 busuanzi_value_site_pv'> </span> </span>
|
||||
<span className='pl-2 hidden busuanzi_container_site_uv'>
|
||||
<i className='fas fa-users'/> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
|
||||
<br/>
|
||||
<h1>{title}</h1>
|
||||
<span className='text-xs '>Powered by <a href='https://github.com/tangly1024/NotionNext' className='underline dark:text-gray-300'>NotionNext {BLOG.VERSION}</a>.</span></span><br/>
|
||||
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
95
themes/matery/components/Header.js
Normal file
95
themes/matery/components/Header.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Typed from 'typed.js'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
let wrapperTop = 0
|
||||
let windowTop = 0
|
||||
let autoScroll = false
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns 头图
|
||||
*/
|
||||
const Header = props => {
|
||||
const [typed, changeType] = useState()
|
||||
const { siteInfo } = props
|
||||
useEffect(() => {
|
||||
scrollTrigger()
|
||||
updateHeaderHeight()
|
||||
if (!typed && window && document.getElementById('typed')) {
|
||||
changeType(
|
||||
new Typed('#typed', {
|
||||
strings: CONFIG_MATERY.HOME_BANNER_GREETINGS,
|
||||
typeSpeed: 200,
|
||||
backSpeed: 100,
|
||||
backDelay: 400,
|
||||
showCursor: true,
|
||||
smartBackspace: true
|
||||
})
|
||||
)
|
||||
}
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
window.addEventListener('resize', updateHeaderHeight)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
window.removeEventListener('resize', updateHeaderHeight)
|
||||
}
|
||||
})
|
||||
|
||||
const autoScrollEnd = () => {
|
||||
if (autoScroll) {
|
||||
windowTop = window.scrollY
|
||||
autoScroll = false
|
||||
}
|
||||
}
|
||||
|
||||
const scrollTrigger = () => {
|
||||
const scrollS = window.scrollY
|
||||
|
||||
// 自动滚动
|
||||
if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll
|
||||
) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
|
||||
setTimeout(autoScrollEnd, 500)
|
||||
}
|
||||
if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
setTimeout(autoScrollEnd, 500)
|
||||
}
|
||||
windowTop = scrollS
|
||||
}
|
||||
|
||||
function updateHeaderHeight () {
|
||||
setTimeout(() => {
|
||||
const wrapperElement = document.getElementById('wrapper')
|
||||
wrapperTop = wrapperElement?.offsetTop
|
||||
}, 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.9), rgba(0,0,0,0.5), rgba(0,0,0,0.3), rgba(0,0,0,0.5), rgba(0, 0, 0, 0.9) ),url("${siteInfo?.pageCover}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex flex-col h-full items-center justify-center w-full ">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed'/>
|
||||
</div>
|
||||
<div onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
|
||||
className="mt-12 border cursor-pointer w-40 text-center pt-4 pb-3 text-md text-white hover:bg-orange-600 duration-300 rounded-3xl">
|
||||
<i className='animate-bounce fas fa-angle-double-down'/> <span>开始阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
69
themes/matery/components/HeaderArticle.js
Normal file
69
themes/matery/components/HeaderArticle.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export default function HeaderArticle({ post, siteInfo }) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!post) {
|
||||
return <></>
|
||||
}
|
||||
const headerImage = post?.page_cover ? `url("${post.page_cover}")` : `url("${siteInfo?.pageCover}")`
|
||||
|
||||
const date = formatDate(
|
||||
post?.date?.start_date || post?.createdTime,
|
||||
locale.LOCALE
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="header"
|
||||
className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat animate__animated animate__fadeIn"
|
||||
style={{ backgroundImage: headerImage }}
|
||||
>
|
||||
<header className="animate__slideInDown animate__animated bg-black bg-opacity-70 absolute top-0 w-full h-96 py-10 flex justify-center items-center ">
|
||||
<div className='mt-24'>
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-xl shadow-text flex justify-center text-center text-white dark:text-white ">
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
<section className="flex-wrap shadow-text flex text-sm justify-center mt-2 text-white dark:text-gray-400 font-light leading-8">
|
||||
<div className='dark:text-gray-200'>
|
||||
{post.category && <>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<div className="cursor-pointer mr-2 dark:hover:text-white hover:underline">
|
||||
<i className="mr-1 fas fa-folder-open" />
|
||||
{post.category}
|
||||
</div>
|
||||
</Link>
|
||||
</>}
|
||||
</div>
|
||||
<div className='flex justify-center'>
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:underline">
|
||||
{locale.COMMON.POST_TIME}: {date}
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<div className="pl-1 mr-2">
|
||||
{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedTime}
|
||||
</div>
|
||||
</div>
|
||||
{BLOG.ANALYTICS_BUSUANZI_ENABLE && <div className="busuanzi_container_page_pv font-light mr-2">
|
||||
<span className="mr-2 busuanzi_value_page_pv" />
|
||||
{locale.COMMON.VIEWS}
|
||||
</div>}
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
43
themes/matery/components/HexoRecentComments.js
Normal file
43
themes/matery/components/HexoRecentComments.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import Card from '@/themes/hexo/components/Card'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { RecentComments } from '@waline/client'
|
||||
|
||||
/**
|
||||
* @see https://waline.js.org/guide/get-started.html
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const HexoRecentComments = (props) => {
|
||||
const [comments, updateComments] = React.useState([])
|
||||
const { locale } = useGlobal()
|
||||
const [onLoading, changeLoading] = React.useState(true)
|
||||
React.useEffect(() => {
|
||||
RecentComments({
|
||||
serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
|
||||
count: 5
|
||||
}).then(({ comments }) => {
|
||||
changeLoading(false)
|
||||
updateComments(comments)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return <Card >
|
||||
<div className=" mb-2 px-1 justify-between">
|
||||
<i className="mr-2 fas fas fa-comment" />
|
||||
{locale.COMMON.RECENT_COMMENTS}
|
||||
</div>
|
||||
|
||||
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
|
||||
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
|
||||
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2 pl-1'>
|
||||
<div className='dark:text-gray-200 text-sm waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
|
||||
<div className='dark:text-gray-400 text-gray-400 text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1 pr-2'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}><a >-- {comment.nick}</a></Link></div>
|
||||
</div>)}
|
||||
|
||||
</Card>
|
||||
}
|
||||
|
||||
export default HexoRecentComments
|
||||
24
themes/matery/components/InfoCard.js
Normal file
24
themes/matery/components/InfoCard.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import Card from './Card'
|
||||
import SocialButton from './SocialButton'
|
||||
import MenuGroupCard from './MenuGroupCard'
|
||||
export function InfoCard (props) {
|
||||
const { className, siteInfo } = props
|
||||
const router = useRouter()
|
||||
return <Card className={className}>
|
||||
<div
|
||||
className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 dark:text-gray-100 transform duration-200 cursor-pointer'
|
||||
onClick={() => {
|
||||
router.push('/')
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={siteInfo?.icon} className='rounded-full' width={120} alt={BLOG.AUTHOR}/>
|
||||
</div>
|
||||
<div className='text-center text-xl pb-4'>{BLOG.AUTHOR}</div>
|
||||
<div className='text-sm text-center'>{BLOG.BIO}</div>
|
||||
<MenuGroupCard {...props}/>
|
||||
<SocialButton />
|
||||
</Card>
|
||||
}
|
||||
29
themes/matery/components/JumpToCommentButton.js
Normal file
29
themes/matery/components/JumpToCommentButton.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
/**
|
||||
* 跳转到评论区
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToCommentButton = () => {
|
||||
if (!CONFIG_MATERY.WIDGET_TO_COMMENT) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
function navToComment() {
|
||||
if (document.getElementById('comment')) {
|
||||
window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' })
|
||||
}
|
||||
// 兼容性不好
|
||||
// const commentElement = document.getElementById('comment')
|
||||
// if (commentElement) {
|
||||
// commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
|
||||
}
|
||||
|
||||
return (<div className='flex space-x-1 items-center justify-center transform hover:scale-105 duration-200 w-7 h-7 text-center' onClick={navToComment} >
|
||||
<i className='fas fa-comment text-xs' />
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default JumpToCommentButton
|
||||
26
themes/matery/components/JumpToTopButton.js
Normal file
26
themes/matery/components/JumpToTopButton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param targetRef 关联高度的目标html标签
|
||||
* @param showPercent 是否显示百分比
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!CONFIG_MATERY.WIDGET_TO_TOP) {
|
||||
return <></>
|
||||
}
|
||||
return (<div className=' drop-shadow-md space-x-1 items-center justify-center transform hover:scale-105 duration-200 px-3 py-2 text-center text-white bg-indigo-700 dark:bg-hexo-black-gray rounded-full'
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
|
||||
<div title={locale.POST.TOP} ><i className='fas fa-arrow-up text-2xl rounded-full' /></div>
|
||||
{showPercent && (<div className='text-md hidden lg:block'>{percent}</div>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default JumpToTopButton
|
||||
66
themes/matery/components/LatestPostsGroup.js
Normal file
66
themes/matery/components/LatestPostsGroup.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 最新文章列表
|
||||
* @param posts 所有文章数据
|
||||
* @param sliceCount 截取展示的数量 默认6
|
||||
* @constructor
|
||||
*/
|
||||
const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
|
||||
// 获取当前路径
|
||||
const currentPath = useRouter().asPath
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!latestPosts) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
|
||||
<div>
|
||||
<i className="mr-2 fas fas fa-history" />
|
||||
{locale.COMMON.LATEST_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
{latestPosts.map(post => {
|
||||
const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
|
||||
const headerImage = post?.page_cover
|
||||
? `url("${post.page_cover}")`
|
||||
: `url("${siteInfo?.pageCover}")`
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a className={'my-2 flex'}>
|
||||
<div
|
||||
className="w-20 h-16 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: headerImage }}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
(selected ? ' text-indigo-400 ' : 'dark:text-gray-400 ') +
|
||||
' text-sm overflow-x-hidden hover:text-indigo-600 px-2 duration-200 w-full rounded ' +
|
||||
'hover:text-white dark:hover:text-indigo-400 cursor-pointer items-center flex'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className='text-line-2'>{post.title}</div>
|
||||
<div className="text-gray-500">{post.lastEditedTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default LatestPostsGroup
|
||||
8
themes/matery/components/LoadingCover.js
Normal file
8
themes/matery/components/LoadingCover.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function LoadingCover () {
|
||||
return (<div id="loading-cover" className={'md:-mt-20 flex-grow dark:text-white text-black animate__animated animate__fadeIn flex flex-col justify-center z-50 w-full h-screen container mx-auto'}>
|
||||
<div className="mx-auto">
|
||||
<i className="fas fa-spinner animate-spin"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
themes/matery/components/Logo.js
Normal file
13
themes/matery/components/Logo.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
const Logo = props => {
|
||||
const { siteInfo } = props
|
||||
return <Link href='/' passHref>
|
||||
<div className='flex flex-col justify-center items-center cursor-pointer space-y-3'>
|
||||
<div className=' text-lg p-1.5 rounded dark:border-white hover:scale-110 transform duration-200'> {siteInfo?.title || BLOG.TITLE}</div>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
export default Logo
|
||||
38
themes/matery/components/MenuButtonGroupTop.js
Normal file
38
themes/matery/components/MenuButtonGroupTop.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
const MenuButtonGroupTop = (props) => {
|
||||
const { customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
let links = [
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_MATERY.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MATERY.MENU_SEARCH }
|
||||
// { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_MATERY.MENU_CATEGORY },
|
||||
// { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MATERY.MENU_TAG }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = customNav.concat(links)
|
||||
}
|
||||
|
||||
return <nav id='nav' className='leading-8 flex justify-center font-light w-full'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
return <Link key={`${link.to}`} title={link.to} href={link.to} >
|
||||
<a target={link.to.indexOf('http') === 0 ? '_blank' : '_self'} className={'py-1.5 my-1 px-3 text-base justify-center items-center cursor-pointer'} >
|
||||
<div className='w-full flex text-sm items-center justify-center hover:scale-125 duration-200 transform'>
|
||||
<i className={`${link.icon} mr-1`}/>
|
||||
<div className='text-center'>{link.name}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
export default MenuButtonGroupTop
|
||||
36
themes/matery/components/MenuGroupCard.js
Normal file
36
themes/matery/components/MenuGroupCard.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
const MenuGroupCard = (props) => {
|
||||
const { postCount, categories, tags } = props
|
||||
const { locale } = useGlobal()
|
||||
const archiveSlot = <div className='text-center'>{postCount}</div>
|
||||
const categorySlot = <div className='text-center'>{categories?.length}</div>
|
||||
const tagSlot = <div className='text-center'>{tags?.length}</div>
|
||||
|
||||
const links = [
|
||||
{ name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG_MATERY.MENU_ARCHIVE },
|
||||
{ name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG_MATERY.MENU_CATEGORY },
|
||||
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG_MATERY.MENU_TAG }
|
||||
]
|
||||
|
||||
return <nav id='nav' className='leading-8 flex justify-center w-full'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
return <Link key={`${link.to}`} title={link.to} href={link.to} >
|
||||
<a target={link.to.indexOf('http') === 0 ? '_blank' : '_self'} className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'} >
|
||||
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'>
|
||||
<div className='text-center'>{link.name}</div>
|
||||
<div className='text-center font-semibold'>{link.slot}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
export default MenuGroupCard
|
||||
44
themes/matery/components/MenuList.js
Normal file
44
themes/matery/components/MenuList.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
const MenuList = (props) => {
|
||||
const { postCount, customNav } = props
|
||||
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>
|
||||
|
||||
let links = [
|
||||
{ icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_MATERY.MENU_CATEGORY },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MATERY.MENU_TAG },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG_MATERY.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MATERY.MENU_SEARCH }
|
||||
]
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-300 '>
|
||||
{links.map(link => {
|
||||
if (link && link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={`${link.to}`} title={link.to} href={link.to} >
|
||||
<a className={'py-1.5 px-5 text-base justify-between hover:bg-indigo-400 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 items-center justify-center flex '>
|
||||
<i className={`${link.icon} w-4 text-center`} />
|
||||
<div className={'ml-4'}>{link.name}</div>
|
||||
</div>
|
||||
{link.slot}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
export default MenuList
|
||||
24
themes/matery/components/NavButtonGroup.js
Normal file
24
themes/matery/components/NavButtonGroup.js
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 首页导航大按钮组件
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const NavButtonGroup = (props) => {
|
||||
const { categories } = props
|
||||
if (!categories || categories.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <nav id='home-nav-button' className={'md:h-52 md:mt-6 xl:mt-32 px-5 py-2 mt-8 flex flex-wrap md:max-w-5xl space-y-2 md:space-y-0 md:flex justify-center max-h-80 overflow-auto'}>
|
||||
{categories.map(category => {
|
||||
return <Link key={`${category.name}`} title={`${category.name}`} href={`/category/${category.name}`} passHref>
|
||||
<a className='text-center w-full md:mx-6 md:w-40 md:h-14 lg:h-20 h-14 justify-center items-center flex border-2 cursor-pointer rounded-lg glassmorphism hover:bg-white hover:text-black duration-200 font-bold hover:scale-110 transform'>{category.name}</a>
|
||||
</Link>
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
export default NavButtonGroup
|
||||
101
themes/matery/components/PaginationNumber.js
Normal file
101
themes/matery/components/PaginationNumber.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 数字翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationNumber = ({ page, totalPage }) => {
|
||||
const router = useRouter()
|
||||
const currentPage = +page
|
||||
const showNext = page < totalPage
|
||||
const pagePrefix = router.asPath.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
||||
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
||||
|
||||
return (
|
||||
<div className="mt-10 mb-5 flex justify-center items-end font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
>
|
||||
<a rel="prev" className={`${currentPage === 1 ? 'invisible' : 'block'} pb-0.5 border-white dark:border-indigo-700 hover:border-indigo-400 dark:hover:border-indigo-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}>
|
||||
<i className="fas fa-angle-left" />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{pages}
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
>
|
||||
<a rel="next" className={`${+showNext ? 'block' : 'invisible'} pb-0.5 border-b border-indigo-300 dark:border-indigo-700 hover:border-indigo-400 dark:hover:border-indigo-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}>
|
||||
<i className="fas fa-angle-right" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getPageElement(page, currentPage, pagePrefix) {
|
||||
return (
|
||||
<Link href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`} key={page} passHref>
|
||||
<a className={
|
||||
(page + '' === currentPage + ''
|
||||
? 'font-bold bg-indigo-400 dark:bg-indigo-500 text-white '
|
||||
: 'border-b duration-500 border-indigo-300 hover:border-indigo-400 ') +
|
||||
' border-white dark:border-indigo-700 dark:hover:border-indigo-400 cursor-pointer pb-0.5 w-6 text-center font-light hover:font-bold'
|
||||
} >
|
||||
{page}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function generatePages(pagePrefix, page, currentPage, totalPage) {
|
||||
const pages = []
|
||||
const groupCount = 7 // 最多显示页签数
|
||||
if (totalPage <= groupCount) {
|
||||
for (let i = 1; i <= totalPage; i++) {
|
||||
pages.push(getPageElement(i, page, pagePrefix))
|
||||
}
|
||||
} else {
|
||||
pages.push(getPageElement(1, page, pagePrefix))
|
||||
const dynamicGroupCount = groupCount - 2
|
||||
let startPage = currentPage - 2
|
||||
if (startPage <= 1) {
|
||||
startPage = 2
|
||||
}
|
||||
if (startPage + dynamicGroupCount > totalPage) {
|
||||
startPage = totalPage - dynamicGroupCount
|
||||
}
|
||||
if (startPage > 2) {
|
||||
pages.push(<div key={-1}>... </div>)
|
||||
}
|
||||
|
||||
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||
if (startPage + i < totalPage) {
|
||||
pages.push(getPageElement(startPage + i, page, pagePrefix))
|
||||
}
|
||||
}
|
||||
|
||||
if (startPage + dynamicGroupCount < totalPage) {
|
||||
pages.push(<div key={-2}>... </div>)
|
||||
}
|
||||
|
||||
pages.push(getPageElement(totalPage, page, pagePrefix))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
export default PaginationNumber
|
||||
57
themes/matery/components/PaginationSimple.js
Normal file
57
themes/matery/components/PaginationSimple.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 简易翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationSimple = ({ page, totalPage }) => {
|
||||
const router = useRouter()
|
||||
const currentPage = +page
|
||||
const showNext = currentPage < totalPage
|
||||
|
||||
return (
|
||||
<div className="my-10 mx-6 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage - 1 === 1
|
||||
? `${BLOG.SUB_PATH || '/'}`
|
||||
: `/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<button
|
||||
rel="prev"
|
||||
className={`${
|
||||
currentPage === 1 ? ' bg-gray-300 text-gray-500 pointer-events-none ' : 'block bg-indigo-700'
|
||||
} duration-200 px-4 py-2 hover:border-black rounded-full`} >
|
||||
<i className='fas fa-angle-left text-2xl'/>
|
||||
</button>
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<button
|
||||
rel="next"
|
||||
className={`${
|
||||
+showNext ? 'text-white bg-indigo-700 ' : ' bg-gray-300 text-gray-500 pointer-events-none '
|
||||
} duration-200 px-4 py-2 hover:border-black rounded-full`}
|
||||
>
|
||||
<i className='fas fa-angle-right text-2xl'/>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationSimple
|
||||
44
themes/matery/components/Progress.js
Normal file
44
themes/matery/components/Progress.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 顶部页面阅读进度条
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || (isBrowser() && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
if (per < 0) per = 0
|
||||
changePercent(per)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
|
||||
return (
|
||||
<div className="h-4 w-full shadow-2xl bg-gray-400 ">
|
||||
<div
|
||||
className="h-4 bg-indigo-400 duration-200"
|
||||
style={{ width: `${percent}%` }}
|
||||
>
|
||||
{showPercent && (
|
||||
<div className="text-right text-white text-xs">{percent}%</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Progress
|
||||
36
themes/matery/components/SearchDrawer.js
Normal file
36
themes/matery/components/SearchDrawer.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Router } from 'next/router'
|
||||
import { useImperativeHandle, useRef } from 'react'
|
||||
import SearchInput from './SearchInput'
|
||||
const SearchDrawer = ({ cRef, slot }) => {
|
||||
const searchDrawer = useRef()
|
||||
const searchInputRef = useRef()
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
show: () => {
|
||||
searchDrawer?.current?.classList?.remove('hidden')
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const hidden = () => {
|
||||
searchDrawer?.current?.classList?.add('hidden')
|
||||
}
|
||||
Router.events.on('routeChangeComplete', (...args) => {
|
||||
hidden()
|
||||
})
|
||||
return (
|
||||
<div id='search-drawer-wrapper' ref={searchDrawer} className='hidden'>
|
||||
<div className='flex-col fixed px-5 w-full left-0 top-14 z-40 justify-center'>
|
||||
<div className='md:max-w-3xl w-full mx-auto animate__animated animate__faster animate__fadeIn'>
|
||||
<SearchInput cRef={searchInputRef} />
|
||||
{slot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 背景蒙版 */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchDrawer
|
||||
106
themes/matery/components/SearchInput.js
Normal file
106
themes/matery/components/SearchInput.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
let lock = false
|
||||
|
||||
const SearchInput = props => {
|
||||
const { currentSearch, cRef, className } = props
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef()
|
||||
const { locale } = useGlobal()
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
const key = searchInputRef.current.value
|
||||
if (key && key !== '') {
|
||||
setLoadingState(true)
|
||||
router.push({ pathname: '/search/' + key }).then(r => {
|
||||
setLoadingState(false)
|
||||
})
|
||||
// location.href = '/search/' + key
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {})
|
||||
}
|
||||
}
|
||||
const handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
// 回车
|
||||
handleSearch(searchInputRef.current.value)
|
||||
} else if (e.keyCode === 27) {
|
||||
// ESC
|
||||
cleanSearch()
|
||||
}
|
||||
}
|
||||
const cleanSearch = () => {
|
||||
searchInputRef.current.value = ''
|
||||
}
|
||||
|
||||
const [showClean, setShowClean] = useState(false)
|
||||
const updateSearchKey = val => {
|
||||
if (lock) {
|
||||
return
|
||||
}
|
||||
searchInputRef.current.value = val
|
||||
|
||||
if (val) {
|
||||
setShowClean(true)
|
||||
} else {
|
||||
setShowClean(false)
|
||||
}
|
||||
}
|
||||
function lockSearchInput () {
|
||||
lock = true
|
||||
}
|
||||
|
||||
function unLockSearchInput () {
|
||||
lock = false
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex w-full rounded-lg ' + className}>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className={
|
||||
'w-full text-sm pl-5 rounded-lg transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'
|
||||
}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
onCompositionEnd={unLockSearchInput}
|
||||
placeholder={locale.SEARCH.ARTICLES}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={currentSearch || ''}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="-ml-8 cursor-pointer float-right items-center justify-center py-2"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
<i
|
||||
className={`hover:text-black transform duration-200 text-gray-500 dark:text-gray-200 cursor-pointer fas ${
|
||||
onLoading ? 'fa-spinner animate-spin' : 'fa-search'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showClean && (
|
||||
<div className="-ml-12 cursor-pointer float-right items-center justify-center py-2">
|
||||
<i
|
||||
className="hover:text-black transform duration-200 text-gray-400 dark:text-gray-300 cursor-pointer fas fa-times"
|
||||
onClick={cleanSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
61
themes/matery/components/SideBar.js
Normal file
61
themes/matery/components/SideBar.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 标签组
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideBar = (props) => {
|
||||
const { siteInfo, customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
const defaultLinks = [
|
||||
{ icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: true },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: true }
|
||||
]
|
||||
let links = [].concat(defaultLinks)
|
||||
if (customNav) {
|
||||
links = defaultLinks.concat(customNav)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='side-bar' className=''>
|
||||
<div className="h-48 w-full bg-indigo-700">
|
||||
<div className='mx-5 pt-6'>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={siteInfo?.icon} className='rounded-full' width={80} alt={BLOG.AUTHOR} />
|
||||
<div className='text-white text-xl my-1'>{siteInfo.title}</div>
|
||||
<div className='text-xs my-1 text-gray-300'>{siteInfo.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
{links.map(link => {
|
||||
if (link && link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={link.to} title={link.to} href={link.to} >
|
||||
<a target={link.to.indexOf('http') === 0 ? '_blank' : '_self'} className={'py-2 px-5 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 items-center justify-between flex '>
|
||||
<i className={`${link.icon} w-4 ml-3 mr-6 text-center`} />
|
||||
<div >{link.name}</div>
|
||||
</div>
|
||||
{link.slot}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
60
themes/matery/components/SideRight.js
Normal file
60
themes/matery/components/SideRight.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import Card from './Card'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import LatestPostsGroup from './LatestPostsGroup'
|
||||
import TagGroups from './TagGroups'
|
||||
import Catalog from './Catalog'
|
||||
import { InfoCard } from './InfoCard'
|
||||
import { AnalyticsCard } from './AnalyticsCard'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import BLOG from '@/blog.config'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const HexoRecentComments = dynamic(() => import('./HexoRecentComments'))
|
||||
/**
|
||||
* Hexo主题右侧栏
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function SideRight(props) {
|
||||
const {
|
||||
post, currentCategory, categories, latestPosts, tags,
|
||||
currentTag, showCategory, showTag, slot
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className={'space-y-4 lg:w-80 lg:pt-0 px-2 pt-4'}>
|
||||
<InfoCard {...props} />
|
||||
{CONFIG_MATERY.WIDGET_ANALYTICS && <AnalyticsCard {...props} />}
|
||||
|
||||
{showCategory && (
|
||||
<Card>
|
||||
<div className='ml-2 mb-1 '>
|
||||
<i className='fas fa-th' /> 分类
|
||||
</div>
|
||||
<CategoryGroup
|
||||
currentCategory={currentCategory}
|
||||
categories={categories}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{showTag && (
|
||||
<Card>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</Card>
|
||||
)}
|
||||
{CONFIG_MATERY.WIDGET_LATEST_POSTS && latestPosts && latestPosts.length > 0 && <Card>
|
||||
<LatestPostsGroup {...props} />
|
||||
</Card>}
|
||||
|
||||
{BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT && <HexoRecentComments/>}
|
||||
|
||||
<div className='sticky top-20'>
|
||||
{post && post.toc && post.toc.length > 1 && <Card>
|
||||
<Catalog toc={post.toc} />
|
||||
</Card>}
|
||||
{slot}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
themes/matery/components/SocialButton.js
Normal file
36
themes/matery/components/SocialButton.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 社交联系方式按钮组
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SocialButton = () => {
|
||||
return <div className='w-full justify-center flex-wrap flex'>
|
||||
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-300 '>
|
||||
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
|
||||
<i className='transform hover:scale-125 duration-150 fab fa-github dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_TWITTER && <a target='_blank' rel='noreferrer' title={'twitter'} href={BLOG.CONTACT_TWITTER} >
|
||||
<i className='transform hover:scale-125 duration-150 fab fa-twitter dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_TELEGRAM && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_TELEGRAM} title={'telegram'} >
|
||||
<i className='transform hover:scale-125 duration-150 fab fa-telegram dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_LINKEDIN && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_LINKEDIN} title={'linkIn'} >
|
||||
<i className='transform hover:scale-125 duration-150 fab fa-linkedin dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_WEIBO && <a target='_blank' rel='noreferrer' title={'weibo'} href={BLOG.CONTACT_WEIBO} >
|
||||
<i className='transform hover:scale-125 duration-150 fab fa-weibo dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
|
||||
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>}
|
||||
<a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
|
||||
<i className='transform hover:scale-125 duration-150 fas fa-rss dark:hover:text-indigo-400 hover:text-indigo-600'/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default SocialButton
|
||||
27
themes/matery/components/TagGroups.js
Normal file
27
themes/matery/components/TagGroups.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
/**
|
||||
* 标签组
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TagGroups = ({ tags, currentTag }) => {
|
||||
if (!tags) return <></>
|
||||
return (
|
||||
<div id='tags-group' className='dark:border-gray-600 space-y-2'>
|
||||
<div className='font-light text-xs ml-2 mb-2'><i className='mr-1 fas fa-tag' />标签</div>
|
||||
<div className='px-4'>
|
||||
{
|
||||
tags.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagGroups
|
||||
12
themes/matery/components/TagItemMini.js
Normal file
12
themes/matery/components/TagItemMini.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItemMini = ({ tag, selected = false }) => {
|
||||
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded-xl hover:text-white duration-200
|
||||
mr-2 py-0.5 px-2 text-xs whitespace-nowrap text-white bg-indigo-700`}>
|
||||
<div className='font-light'>{selected && <i className='mr-1 fa-tag'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
export default TagItemMini
|
||||
42
themes/matery/components/TocDrawer.js
Normal file
42
themes/matery/components/TocDrawer.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Catalog from './Catalog'
|
||||
import React, { useImperativeHandle, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 目录抽屉栏
|
||||
* @param toc
|
||||
* @param post
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TocDrawer = ({ post, cRef }) => {
|
||||
// 暴露给父组件 通过cRef.current.handleMenuClick 调用
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
handleSwitchVisible: () => switchVisible()
|
||||
}
|
||||
})
|
||||
const [showDrawer, switchShowDrawer] = useState(false)
|
||||
const switchVisible = () => {
|
||||
switchShowDrawer(!showDrawer)
|
||||
}
|
||||
return <>
|
||||
<div className='fixed top-0 right-0 z-40 '>
|
||||
{/* 侧边菜单 */}
|
||||
<div
|
||||
className={(showDrawer ? 'animate__slideInRight ' : ' -mr-72 animate__slideOutRight') +
|
||||
' shadow-card animate__animated animate__faster' +
|
||||
' w-60 duration-200 fixed right-12 bottom-12 rounded py-2 bg-white dark:bg-gray-600'}>
|
||||
{post && <>
|
||||
<div className='dark:text-gray-400 text-gray-600'>
|
||||
<Catalog toc={post.toc}/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{/* 背景蒙版 */}
|
||||
<div id='right-drawer-background' className={(showDrawer ? 'block' : 'hidden') + ' fixed top-0 left-0 z-30 w-full h-full'}
|
||||
onClick={switchVisible} />
|
||||
</>
|
||||
}
|
||||
export default TocDrawer
|
||||
22
themes/matery/components/TocDrawerButton.js
Normal file
22
themes/matery/components/TocDrawerButton.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
|
||||
/**
|
||||
* 点击召唤目录抽屉
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param props 父组件传入props
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TocDrawerButton = (props) => {
|
||||
if (!CONFIG_MATERY.WIDGET_TOC) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return (<div onClick={props.onClick} className='py-2 px-3 cursor-pointer transform duration-200 flex justify-center items-center w-7 h-7 text-center' title={locale.POST.TOP} >
|
||||
<i className='fas fa-list-ol text-xs'/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TocDrawerButton
|
||||
159
themes/matery/components/TopNav.js
Normal file
159
themes/matery/components/TopNav.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import Logo from './Logo'
|
||||
import SearchDrawer from './SearchDrawer'
|
||||
import TagGroups from './TagGroups'
|
||||
import MenuButtonGroupTop from './MenuButtonGroupTop'
|
||||
import { useRouter } from 'next/router'
|
||||
import SideBarDrawer from '@/components/SideBarDrawer'
|
||||
import SideBar from './SideBar'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
/**
|
||||
* 顶部导航
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = props => {
|
||||
const { tags, currentTag, categories, currentCategory } = props
|
||||
const { locale } = useGlobal()
|
||||
const searchDrawer = useRef()
|
||||
const { isDarkMode } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
const scrollTrigger = throttle(() => {
|
||||
const scrollS = window.scrollY
|
||||
const nav = document.querySelector('#sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
const showNav = scrollS <= windowTop || scrollS < 5 || (header && scrollS <= header.clientHeight)// 非首页无大图时影藏顶部 滚动条置顶时隐藏
|
||||
// 是否将导航栏透明
|
||||
const navTransparent = (scrollS < document.documentElement.clientHeight - 12 && router.route === '/') || scrollS < 300 // 透明导航条的条件
|
||||
|
||||
if (header && navTransparent) {
|
||||
nav && nav.classList.replace('bg-indigo-700', 'bg-none')
|
||||
nav && nav.classList.replace('text-black', 'text-white')
|
||||
nav && nav.classList.replace('border', 'border-transparent')
|
||||
nav && nav.classList.replace('shadow-sm', 'shadow-none')
|
||||
nav && nav.classList.replace('dark:bg-hexo-black-gray', 'transparent')
|
||||
} else {
|
||||
nav && nav.classList.replace('bg-none', 'bg-indigo-700')
|
||||
nav && nav.classList.replace('text-white', 'text-black')
|
||||
nav && nav.classList.replace('border-transparent', 'border')
|
||||
nav && nav.classList.replace('shadow-none', 'shadow-sm')
|
||||
nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray')
|
||||
}
|
||||
|
||||
if (!showNav) {
|
||||
nav && nav.classList.replace('top-0', '-top-20')
|
||||
windowTop = scrollS
|
||||
} else {
|
||||
nav && nav.classList.replace('-top-20', 'top-0')
|
||||
windowTop = scrollS
|
||||
}
|
||||
navDarkMode()
|
||||
}, 200)
|
||||
|
||||
const navDarkMode = () => {
|
||||
const nav = document.getElementById('sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
if (!isDarkMode && nav && header) {
|
||||
if (window.scrollY < header.clientHeight) {
|
||||
nav?.classList?.add('dark')
|
||||
} else {
|
||||
nav?.classList?.remove('dark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
scrollTrigger()
|
||||
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
const toggleMenuClose = () => {
|
||||
changeShow(false)
|
||||
}
|
||||
|
||||
const searchDrawerSlot = <>
|
||||
{ categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm flex flex-nowrap justify-between font-light px-2'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-th-list' />{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} <i className='fas fa-angle-double-right' />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</section>
|
||||
) }
|
||||
|
||||
{ tags && (
|
||||
<section className='mt-4'>
|
||||
<div className='text-sm py-2 px-2 flex flex-nowrap justify-between font-light dark:text-gray-200'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-tag'/>{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} <i className='fas fa-angle-double-right' />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</section>
|
||||
) }
|
||||
</>
|
||||
|
||||
return (<div id='top-nav'>
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'flex justify-center top-0 shadow-black shadow-sm fixed bg-none animate__animated animate__fadeIn dark:bg-hexo-black-gray text-gray-200 w-full z-30 transform duration-200'}>
|
||||
<div className='w-full max-w-6xl flex justify-between items-center px-4 py-2'>
|
||||
{/* 左侧功能 */}
|
||||
<div className='justify-start items-center block lg:hidden '>
|
||||
<div onClick={toggleMenuOpen} className='w-8 justify-center items-center h-8 cursor-pointer flex lg:hidden'>
|
||||
{ isOpen ? <i className='fas fa-times'/> : <i className='fas fa-bars'/> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex'>
|
||||
<Logo {...props}/>
|
||||
</div>
|
||||
|
||||
{/* 右侧功能 */}
|
||||
<div className='mr-1 justify-end items-center '>
|
||||
<div className='hidden lg:flex'> <MenuButtonGroupTop {...props}/></div>
|
||||
<Link href={'/search'}>
|
||||
<a><i className='fas fa-search'/></a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<SideBarDrawer isOpen={isOpen} onClose={toggleMenuClose}>
|
||||
<SideBar {...props}/>
|
||||
</SideBarDrawer>
|
||||
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
28
themes/matery/config_matery.js
Normal file
28
themes/matery/config_matery.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const CONFIG_MATERY = {
|
||||
HOME_BANNER_ENABLE: true,
|
||||
HOME_BANNER_GREETINGS: ['Hi,我是一个程序员', 'Hi,我是一个打工人', 'Hi,我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
|
||||
|
||||
HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮
|
||||
|
||||
// 菜单配置
|
||||
MENU_CATEGORY: true, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: true, // 显示归档
|
||||
MENU_SEARCH: true, // 显示搜索
|
||||
|
||||
POST_LIST_COVER: true, // 文章封面
|
||||
POST_LIST_SUMMARY: true, // 文章摘要
|
||||
POST_LIST_PREVIEW: true, // 读取文章预览
|
||||
|
||||
ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐
|
||||
ARTICLE_COPYRIGHT: true, // 显示文章版权声明
|
||||
ARTICLE_RECOMMEND: true, // 文章关联推荐
|
||||
|
||||
WIDGET_LATEST_POSTS: true, // 显示最新文章卡
|
||||
WIDGET_ANALYTICS: false, // 显示统计卡
|
||||
WIDGET_TO_TOP: true,
|
||||
WIDGET_TO_COMMENT: true, // 跳到评论区
|
||||
WIDGET_DARK_MODE: true, // 夜间模式
|
||||
WIDGET_TOC: true // 移动端悬浮目录
|
||||
}
|
||||
export default CONFIG_MATERY
|
||||
25
themes/matery/index.js
Normal file
25
themes/matery/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import CONFIG_MATERY from './config_matery'
|
||||
import { LayoutIndex } from './LayoutIndex'
|
||||
import { LayoutSearch } from './LayoutSearch'
|
||||
import { LayoutArchive } from './LayoutArchive'
|
||||
import { LayoutSlug } from './LayoutSlug'
|
||||
import { Layout404 } from './Layout404'
|
||||
import { LayoutCategory } from './LayoutCategory'
|
||||
import { LayoutCategoryIndex } from './LayoutCategoryIndex'
|
||||
import { LayoutPage } from './LayoutPage'
|
||||
import { LayoutTag } from './LayoutTag'
|
||||
import { LayoutTagIndex } from './LayoutTagIndex'
|
||||
|
||||
export {
|
||||
CONFIG_MATERY as THEME_CONFIG,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
LayoutSlug,
|
||||
Layout404,
|
||||
LayoutCategory,
|
||||
LayoutCategoryIndex,
|
||||
LayoutPage,
|
||||
LayoutTag,
|
||||
LayoutTagIndex
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import React from 'react'
|
||||
import Header from './components/Header'
|
||||
import Nav from './components/Nav'
|
||||
import { Footer } from './components/Footer'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
/**
|
||||
@@ -18,8 +18,8 @@ const LayoutBase = props => {
|
||||
<div className='nobelium dark:text-gray-300 w-full overflow-hidden bg-white dark:bg-black min-h-screen'>
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
{/* 顶栏LOGO */}
|
||||
<Header {...props} />
|
||||
{/* 顶部导航栏 */}
|
||||
<Nav {...props} />
|
||||
|
||||
<main className={`m-auto flex-grow w-full transition-all ${
|
||||
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const NavBar = (props) => {
|
||||
const { customNav } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ id: 2, name: locale.NAV.RSS, to: '/feed', show: true },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: true },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: true },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: false },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: true }
|
||||
]
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<ul className="flex flex-row">
|
||||
{links.map(
|
||||
link =>
|
||||
link.show && (
|
||||
<li
|
||||
key={link.id}
|
||||
className="block ml-4 text-black dark:text-gray-50 nav"
|
||||
>
|
||||
<Link href={link.to}>
|
||||
<a>{link.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ navBarTitle, fullWidth }) => {
|
||||
const useSticky = !BLOG.autoCollapsedNavBar
|
||||
const navRef = useRef(null)
|
||||
const sentinalRef = useRef([])
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current && useSticky) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current?.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current?.classList.remove('sticky-nav-full')
|
||||
}
|
||||
} else {
|
||||
navRef.current?.classList.add('remove-sticky')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const obvserver = new window.IntersectionObserver(handler)
|
||||
obvserver.observe(sentinalRef.current)
|
||||
// Don't touch this, I have no idea how it works XD
|
||||
// return () => {
|
||||
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
// }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sentinalRef])
|
||||
return (
|
||||
<>
|
||||
<div className="observer-element h-4 md:h-12" ref={sentinalRef}></div>
|
||||
<div
|
||||
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 md:mb-12 py-8 bg-opacity-60 ${
|
||||
!fullWidth ? 'max-w-3xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
id="sticky-nav"
|
||||
ref={navRef}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Link href="/">
|
||||
<a aria-label={BLOG.title}>
|
||||
<div className="h-6">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
className="fill-current text-black dark:text-white"
|
||||
/>
|
||||
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45) scale(39.598)"
|
||||
>
|
||||
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
||||
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className="ml-2 font-medium text-day dark:text-night header-name">
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className="ml-2 font-medium text-day dark:text-night header-name">
|
||||
{BLOG.title},{' '}
|
||||
<span className="font-normal">{BLOG.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,39 +1,128 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 菜单导航
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Nav = (props) => {
|
||||
const Nav = ({ navBarTitle, fullWidth }) => {
|
||||
const useSticky = !BLOG.autoCollapsedNavBar
|
||||
const navRef = useRef(null)
|
||||
const sentinalRef = useRef([])
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current && useSticky) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current?.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current?.classList.remove('sticky-nav-full')
|
||||
}
|
||||
} else {
|
||||
navRef.current?.classList.add('remove-sticky')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const obvserver = new window.IntersectionObserver(handler)
|
||||
obvserver.observe(sentinalRef.current)
|
||||
// Don't touch this, I have no idea how it works XD
|
||||
// return () => {
|
||||
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
// }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sentinalRef])
|
||||
return (
|
||||
<>
|
||||
<div className="observer-element h-4 md:h-12" ref={sentinalRef}></div>
|
||||
<div
|
||||
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 md:mb-12 py-8 bg-opacity-60 ${
|
||||
!fullWidth ? 'max-w-3xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
id="sticky-nav"
|
||||
ref={navRef}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Link href="/">
|
||||
<a aria-label={BLOG.title}>
|
||||
<div className="h-6">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
className="fill-current text-black dark:text-white"
|
||||
/>
|
||||
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45) scale(39.598)"
|
||||
>
|
||||
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
||||
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className="ml-2 font-medium text-day dark:text-night header-name">
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className="ml-2 font-medium text-day dark:text-night header-name">
|
||||
{BLOG.title},{' '}
|
||||
<span className="font-normal">{BLOG.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const NavBar = (props) => {
|
||||
const { customNav } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' }
|
||||
{ id: 2, name: locale.NAV.RSS, to: '/feed', show: true },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: true },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: true },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: false },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: true }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
return <nav className="w-full bg-white md:pt-0 px-6 relative z-20 border-t border-b border-gray-light dark:border-hexo-black-gray dark:bg-black">
|
||||
<div className="container mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start">
|
||||
<div className="w-full md:w-2/3 text-center md:text-left py-4 flex flex-wrap justify-center items-stretch md:justify-start md:items-start">
|
||||
{links.map(link => {
|
||||
return link && <Link href={link.to} key={link.to}>
|
||||
<a className="px-2 md:pl-0 md:mr-3 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light">
|
||||
{link.name}
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
<div className="w-full md:w-1/3 text-center md:text-right">
|
||||
{/* <!-- extra links --> */}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<ul className="flex flex-row">
|
||||
{links.map(
|
||||
link =>
|
||||
link.show && (
|
||||
<li
|
||||
key={link.id}
|
||||
className="block ml-4 text-black dark:text-gray-50 nav"
|
||||
>
|
||||
<Link href={link.to}>
|
||||
<a>{link.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CONFIG_EMPTY = {
|
||||
const CONFIG_NOBELIUM = {
|
||||
TEST_CONFIG: 'TESET'
|
||||
}
|
||||
export default CONFIG_EMPTY
|
||||
export default CONFIG_NOBELIUM
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import CONFIG_EMPTY from './config_nobelium'
|
||||
import CONFIG_NOBELIUM from './config_nobelium'
|
||||
import { LayoutIndex } from './LayoutIndex'
|
||||
import { LayoutSearch } from './LayoutSearch'
|
||||
import { LayoutArchive } from './LayoutArchive'
|
||||
@@ -11,7 +11,7 @@ import { LayoutTag } from './LayoutTag'
|
||||
import { LayoutTagIndex } from './LayoutTagIndex'
|
||||
|
||||
export {
|
||||
CONFIG_EMPTY as THEME_CONFIG,
|
||||
CONFIG_NOBELIUM as THEME_CONFIG,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
|
||||
Reference in New Issue
Block a user