Merge pull request #633 from tangly1024/feat/new-theme-matery

Feat/new theme matery
This commit is contained in:
tangly1024
2022-12-25 18:43:51 +08:00
committed by GitHub
81 changed files with 2915 additions and 241 deletions

View File

@@ -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)
}

View File

@@ -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

View File

@@ -13,7 +13,8 @@ export function ThemeSwitch() {
<div id="draggableBox" style={{ left: '10px', top: '90vh' }} className="fixed text-white bg-black z-50 rounded-lg shadow-card">
<div className="p-2 flex items-center">
<i className="fas fa-palette mr-1 cursor-move" />
<div className='uppercase font-sans whitespace-nowrap' >{theme} <i className='fas fa-sync cursor-pointer border-l pl-2' title='click to change theme' alt='click to change theme' onClick={switchTheme} /></div>
<div className='uppercase font-sans whitespace-nowrap cursor-pointer ' onClick={switchTheme}> {theme}</div>
<i className='fas fa-arrows cursor-move pl-2' title='click to change theme' alt='click to change theme' />
</div>
</div>
</Draggable>

View File

@@ -25,6 +25,7 @@
"@popperjs/core": "^2.9.3",
"animate.css": "^4.1.1",
"animejs": "^3.2.1",
"aos": "^3.0.0-beta.6",
"axios": ">=0.21.1",
"copy-to-clipboard": "^3.3.1",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -58,7 +59,7 @@
},
"devDependencies": {
"@waline/client": "^2.5.1",
"autoprefixer": "^10.2.5",
"autoprefixer": "^10.4.13",
"eslint": "^7.26.0",
"eslint-config-next": "^11.0.0",
"eslint-config-standard": "^16.0.2",
@@ -67,8 +68,8 @@
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"next-sitemap": "^1.6.203",
"postcss": "^8.2.15",
"tailwindcss": "^2.1.2",
"postcss": "^8.4.20",
"tailwindcss": "^3.2.4",
"webpack-bundle-analyzer": "^4.5.0"
},
"resolutions": {

View File

@@ -2,7 +2,7 @@ const BLOG = require('./blog.config')
const { fontFamilies } = require('./lib/font')
module.exports = {
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.js'],
content: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.js'],
darkMode: BLOG.APPEARANCE === 'class' ? 'media' : 'class', // or 'media' or 'class'
theme: {
fontFamily: fontFamilies,

View File

@@ -12,6 +12,9 @@ import LoadingCover from './components/LoadingCover'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import FacebookPage from '@/components/FacebookPage'
import AOS from 'aos'
import 'aos/dist/aos.css' // You can also use <link> for styles
import { isBrowser } from '@/lib/utils'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -51,6 +54,10 @@ const LayoutBase = props => {
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
if (isBrowser()) {
AOS.init()
}
return (
<div className="bg-hexo-background-gray dark:bg-black">
<CommonHead meta={meta} siteInfo={siteInfo}/>

View File

@@ -8,7 +8,13 @@ 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
data-aos="fade-up"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className="w-full drop-shadow-md 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"

View File

@@ -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>

View File

@@ -3,11 +3,12 @@ import { saveDarkModeToCookies } from '@/lib/theme'
import CONFIG_HEXO from '../config_hexo'
export default function FloatDarkModeButton () {
const { isDarkMode, updateDarkMode } = useGlobal()
if (!CONFIG_HEXO.WIDGET_DARK_MODE) {
return <></>
}
const { isDarkMode, updateDarkMode } = useGlobal()
// 用户手动设置主题
const handleChangeDarkMode = () => {
const newStatus = !isDarkMode

View File

@@ -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}/>

View File

@@ -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 }

View 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>
)
}

View 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>
}

View File

@@ -0,0 +1,92 @@
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'
import AOS from 'aos'
import 'aos/dist/aos.css' // You can also use <link> for styles
import FloatDarkModeButton from './components/FloatDarkModeButton'
import { isBrowser } from '@/lib/utils'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @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 > 300 && per > 0
if (shouldShow !== show) {
switchShow(shouldShow)
}
// changePercent(per)
}
useEffect(() => {
smoothscroll.polyfill()
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
if (isBrowser()) {
AOS.init()
}
return (
<div id="outer-wrapper" className="min-h-screen flex flex-col justify-between bg-hexo-background-gray dark:bg-black w-full overflow-hidden">
<CommonHead meta={meta} siteInfo={siteInfo} />
<TopNav {...props} />
{headerSlot}
<main id="wrapper" className="flex-1 w-full py-8 md:px-8 lg:px-24">
<div id="container-inner" className="w-full max-w-6xl mx-auto lg:flex lg:space-x-4 justify-center">
{onLoading ? <LoadingCover /> : children}
</div>
</main>
{/* 左下角悬浮 */}
<div className="bottom-4 -left-14 fixed justify-end z-40">
<Live2D />
</div>
<div className="bottom-40 right-2 fixed justify-end z-20">
<FloatDarkModeButton />
</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

View 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>
}

View 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>
)
}

View 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>
}

View 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>
}

View 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 mt-12">
<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>
)
}

109
themes/matery/LayoutSlug.js Normal file
View File

@@ -0,0 +1,109 @@
import React from 'react'
import { ArticleLock } from './components/ArticleLock'
import HeaderArticle from './components/HeaderArticle'
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 { ArticleInfo } from './components/ArticleInfo'
import Catalog from './components/Catalog'
import JumpToCommentButton from './components/JumpToCommentButton'
export const LayoutSlug = props => {
const { post, lock, validPassword } = props
const [show, switchShow] = React.useState(false)
const scrollListener = () => {
const scrollY = window.pageYOffset
const shouldShow = scrollY > 220
if (shouldShow !== show) {
switchShow(shouldShow)
}
}
React.useEffect(() => {
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
if (!post) {
return <LayoutBase
headerSlot={<HeaderArticle {...props} />}
{...props}
showCategory={false}
showTag={false}
></LayoutBase>
}
return (
<LayoutBase
headerSlot={<HeaderArticle {...props} />}
{...props}
showCategory={false}
showTag={false}
>
<div id='inner-wrapper' className='flex'>
<div className='drop-shadow-xl max-w-4xl 2xl:ml-52'>
<div className="-mt-32 rounded-md mx-3 lg:border lg:rounded-xl 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 md:w-full ">
{post?.type === 'Post' && <>
<div className='px-10'>
<ArticleInfo post={post} />
</div>
<hr />
</>}
<div className='lg:px-10 '>
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased" >
{/* Notion文章主体 */}
<section id='notion-article' className='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} />}
</article>
<hr className='border-dashed' />
{/* 评论互动 */}
<div className="duration-200 overflow-x-auto dark:bg-hexo-black-gray px-3">
<Comment frontMatter={post} />
</div>
</div>
</div>}
</div>
{post.type === 'Post' && <ArticleAdjacent {...props} />}
</div>
<div className='fixed bottom-28 right-4'>
<JumpToCommentButton />
</div>
<div id='toc-widget' className='w-60 hidden xl:block '>
<div className='fixed top-24 overflow-auto'>
{show && <Catalog toc={post.toc} />}
</div>
</div>
</div>
</LayoutBase>
)
}

View File

@@ -0,0 +1,40 @@
import BLOG from '@/blog.config'
import BlogPostListScroll from './components/BlogPostListScroll'
import BlogPostListPage from './components/BlogPostListPage'
import LayoutBase from './LayoutBase'
import React from 'react'
import HeaderArticle from './components/HeaderArticle'
import { useGlobal } from '@/lib/global'
import TagItemMiddle from './components/TagItemMiddle'
export const LayoutTag = (props) => {
const { tags, tag } = props
const { locale } = useGlobal()
return <LayoutBase {...props} headerSlot={<HeaderArticle {...props} />} >
<div className='inner-wrapper drop-shadow-xl'>
<div className="-mt-32 rounded-md mx-3 px-5 lg:border lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
<div className="dark:text-gray-200 py-5 text-center text-2xl">
<i className="fas fa-tags" /> {locale.COMMON.TAGS}
</div>
<div id="tags-list" className="duration-200 flex flex-wrap justify-center pb-12">
{tags.map(e => {
const selected = tag === e.name
return (
<div key={e.id} className="p-2">
<TagItemMiddle key={e.id} tag={e} selected={selected} />
</div>
)
})}
</div>
</div>
</div>
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
</LayoutBase>
}

View File

@@ -0,0 +1,32 @@
import { useGlobal } from '@/lib/global'
import HeaderArticle from './components/HeaderArticle'
import TagItemMiddle from './components/TagItemMiddle'
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = props => {
const { tags } = props
const { locale } = useGlobal()
return (
<LayoutBase {...props} headerSlot={<HeaderArticle {...props} />} >
<div className='inner-wrapper drop-shadow-xl'>
<div className="-mt-32 rounded-md mx-3 px-5 lg:border lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
<div className="dark:text-gray-200 py-5 text-center text-2xl">
<i className="fas fa-tags" /> {locale.COMMON.TAGS}
</div>
<div id="tags-list" className="duration-200 flex flex-wrap justify-center pb-12">
{tags.map(tag => {
return (
<div key={tag.name} className="p-2">
<TagItemMiddle key={tag.name} tag={tag} />
</div>
)
})}
</div>
</div>
</div>
</LayoutBase>
)
}

View 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>
}

View File

@@ -0,0 +1,19 @@
import CONFIG_MATERY from '../config_matery'
import BlogPostCard from './BlogPostCard'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleAdjacent ({ prev, next }) {
if (!prev || !next || !CONFIG_MATERY.ARTICLE_ADJACENT) {
return <></>
}
return <section className='flex flex-col justify-between p-3 text-gray-800 items-center text-xs md:text-sm md:flex-row md:gap-2 '>
<BlogPostCard post={prev}/>
<BlogPostCard post={next}/>
</section>
}

View 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>
}

View File

@@ -0,0 +1,46 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import formatDate from '@/lib/formatDate'
import TagItemMiddle from './TagItemMiddle'
import WordCount from './WordCount'
export const ArticleInfo = (props) => {
const { post } = props
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
return <section className='mb-3 dark:text-gray-200'>
<div className='my-3'>
{post.tagItems && (
<div className="flex flex-nowrap overflow-x-auto">
{post.tagItems.map(tag => (
<TagItemMiddle key={tag.name} tag={tag} />
))}
</div>
)}
</div>
<div className='flex flex-wrap gap-3 mt-5 text-sm'>
{post?.type !== 'Page' && (<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
>
<a className="cursor-pointer whitespace-nowrap">
<i className='far fa-calendar-minus fa-fw'/>发布日期: {date}
</a>
</Link>
<span className='whitespace-nowrap'>
<i className='far fa-calendar-check fa-fw' /> 更新日期: {post.lastEditedTime}
</span>
<span className="hidden busuanzi_container_page_pv font-light mr-2">
<i className='mr-1 fas fa-eye' />
<span className="busuanzi_value_page_pv" />
</span>
<WordCount />
</>)}
</div>
</section>
}

View 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'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,68 @@
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 }) {
const { locale } = useGlobal()
if (
!CONFIG_MATERY.ARTICLE_RECOMMEND ||
!recommendPosts ||
recommendPosts.length === 0
) {
return <></>
}
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>
)
}

View 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>{' '}
&nbsp;
<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

View File

@@ -0,0 +1,94 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import React from 'react'
import TagItemMini from './TagItemMini'
import CONFIG_MATERY from '../config_matery'
const BlogPostCard = ({ post, showSummary }) => {
const showPreview = CONFIG_MATERY.POST_LIST_PREVIEW && post.blockMap
return (
<div
data-aos="fade-up"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className="w-full mb-4 h-full overflow-auto drop-shadow-md border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray">
{/* 固定高度 ,空白用图片拉升填充 */}
<div key={post.id} className="flex flex-col h-96 justify-between">
{/* 头部图片 填充卡片 */}
{CONFIG_MATERY.POST_LIST_COVER && !showPreview && post?.page_cover && (
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref>
<div className="flex flex-grow w-full relative duration-200 bg-black rounded-t-md 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 h-full w-full hover:scale-125 rounded-t-md transform object-cover duration-500"
/>
<span className='absolute bottom-0 left-0 text-white p-6 text-2xl replace' > {post.title}</span>
</div>
</Link>
)}
<div >
{/* 描述 */}
<div className="px-4 flex flex-col w-full text-gray-700 dark:text-gray-300">
{(!showPreview || showSummary) && !post.results && post.summary && (
<p style={{ overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: '4', WebkitBoxOrient: 'vertical' }}
className="replace my-2 text-sm font-light leading-7">
{post.summary}
</p>
)}
{/* 搜索结果 */}
{post.results && (
<p className="mt-4 replace text-sm font-light leading-7">
{post.results.map(r => (
<span key={r}>{r}</span>
))}
</p>
)}
<div className='text-gray-800 justify-between flex my-2 dark:text-gray-300'>
<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>
</div>
)
}
export default BlogPostCard

View 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

View File

@@ -0,0 +1,35 @@
import BlogPostCard from './BlogPostCard'
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='pt-6'></div>
{/* 文章列表 */}
<div className="px-4 pt-4 xl:columns-3 md:columns-2 pb-24" >
{posts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
{showPagination && <PaginationSimple page={page} totalPage={totalPage} />}
</div>
)
}
}
export default BlogPostListPage

View 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

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
<>{headerSlot}</>
<section className="drop-shadow-xl 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

View File

@@ -0,0 +1,92 @@
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 }) => {
// 监听滚动事件
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))
// 无目录就直接返回空
if (!toc || toc.length < 1) {
return <></>
}
return <div className='px-3 w-72'>
<div className='dark:text-white'><i className='mr-1 fas fa-stream' /> 目录</div>
<div className='w-full py-3'>
<Progress />
</div>
<div className='overflow-y-auto 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-green-500 underline'}`}
>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</div>
</div>
}
export default Catalog

View 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

View 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

View File

@@ -0,0 +1,29 @@
import { useGlobal } from '@/lib/global'
import { saveDarkModeToCookies } from '@/lib/theme'
import CONFIG_MATERY from '../config_matery'
export default function FloatDarkModeButton() {
const { isDarkMode, updateDarkMode } = useGlobal()
if (!CONFIG_MATERY.WIDGET_DARK_MODE) {
return <></>
}
// 用户手动设置主题
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 className={'justify-center items-center text-center' } onClick={handleChangeDarkMode}>
<i id="darkModeButton"
className={`${isDarkMode ? 'fa-sun' : 'fa-moon'} fas transform hover:scale-105 duration-200
text-2xl text-white bg-indigo-700 px-3 py-2.5 rounded-full dark:bg-black cursor-pointer`} />
</div>
)
}

View 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

View 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

View File

@@ -0,0 +1,17 @@
export default function HeaderArticle({ post, siteInfo }) {
const headerImage = post?.page_cover ? `url("${post?.page_cover}")` : `url("${siteInfo?.pageCover}")`
const title = post?.title
return (
<div
id="header"
className="w-full h-80 md:flex-shrink-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: headerImage }}
>
<div className="flex flex-col h-80 justify-center ">
<div className="font-bold text-xl shadow-text flex justify-center text-center text-white dark:text-white ">
{title}
</div>
</div>
</div>
)
}

View 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

View 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>
}

View File

@@ -0,0 +1,27 @@
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' })
}
}
return (<div
onClick={navToComment}
className='flex space-x-1 items-center justify-center cursor-pointer transform hover:scale-105 duration-200 w-7 h-7 text-center'>
<i className='fas fa-comments text-xl text-white bg-indigo-700 py-3 px-2 rounded-full' />
</div>)
}
export default JumpToCommentButton

View 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

View 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

View 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>
)
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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 text-white bg-indigo-700'
} duration-200 px-3.5 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

View 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

View 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

View 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

View File

@@ -0,0 +1,62 @@
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='cursor-pointer 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-indigo-500 text-white ' : ' text-black dark:text-white ')} >
<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

View 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>
)
}

View 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

View 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

View File

@@ -0,0 +1,14 @@
import Link from 'next/link'
const TagItemMiddle = ({ 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-md whitespace-nowrap text-white ${selected ? 'bg-black' : 'bg-indigo-700'}`}>
<div className='font-light'>
{selected && <i className='mr-1 fas fa-tag' />}
{tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a>
</Link>
}
export default TagItemMiddle

View 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

View 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

View 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

View 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>
<div className='block lg:hidden'><Link href={'/search'}>
<a><i className='fas fa-search' /></a>
</Link></div>
</div>
</div>
</div>
<SideBarDrawer isOpen={isOpen} onClose={toggleMenuClose}>
<SideBar {...props} />
</SideBarDrawer>
</div>)
}
export default TopNav

View File

@@ -0,0 +1,65 @@
import { useEffect } from 'react'
/**
* 字数统计
* @returns
*/
export default function WordCount() {
useEffect(() => {
countWords()
})
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
<span className='flex whitespace-nowrap'>
<i className='mr-1 fas fa-file-word' />
<span>文章字数</span>
<span id='wordCount'>0</span></span>
<span className='flex whitespace-nowrap'>
<i className='mr-1 fas fa-clock' />
<span>阅读时长:</span>
<span id='readTime'>0</span>
</span>
</span>
}
/**
* 更新字数统计和阅读时间
*/
function countWords() {
const articleText = deleteHtmlTag(document.getElementById('notion-article')?.innerHTML)
const wordCount = fnGetCpmisWords(articleText)
// 阅读速度 300-500每分钟
document.getElementById('wordCount').innerHTML = wordCount
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
const wordCountWrapper = document.getElementById('wordCountWrapper')
wordCountWrapper.classList.remove('hidden')
}
// 去除html标签
function deleteHtmlTag(str) {
if (!str) {
return ''
}
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和&nbsp;之类的特殊符合
return str
}
// 用word方式计算正文字数
function fnGetCpmisWords(str) {
if (!str) {
return 0
}
let sLen = 0
try {
// eslint-disable-next-line no-irregular-whitespace
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
// eslint-disable-next-line no-control-regex
str = str.replace(/[\x00-\xff]/g, 'm')
str = str.replace(/m+/g, '*')
str = str.replace(/龘+/g, '')
sLen = str.length
} catch (e) {
}
return sLen
}

View 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
View 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
}

View File

@@ -13,6 +13,9 @@ import React from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import CONFIG_NEXT from './config_next'
import Live2D from '@/components/Live2D'
import AOS from 'aos'
import 'aos/dist/aos.css' // You can also use <link> for styles
import { isBrowser } from '@/lib/utils'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -58,6 +61,10 @@ const LayoutBase = (props) => {
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
if (isBrowser()) {
AOS.init()
}
return (<>
<CommonHead meta={meta} />
@@ -79,7 +86,7 @@ const LayoutBase = (props) => {
</main>
{/* 右下角悬浮 */}
<div ref={floatButtonGroup} className='right-8 bottom-12 bottom-24 lg:right-2 fixed justify-end z-20 font-sans'>
<div ref={floatButtonGroup} className='right-8 bottom-12 lg:right-2 fixed justify-end z-20 font-sans'>
<div className={(show ? 'animate__animated ' : 'hidden') + ' animate__fadeInUp rounded-md glassmorphism justify-center duration-500 animate__faster flex space-x-2 items-center cursor-pointer '}>
<JumpToTopButton percent={percent}/>
<JumpToBottomButton />

View File

@@ -25,7 +25,14 @@ export default function ArticleDetail(props) {
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
return (<div id="container" className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
return (<div id="container"
data-aos="fade-down"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
<div itemScope itemType="https://schema.org/Movie"
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-hexo-black-gray"
>
@@ -50,7 +57,7 @@ export default function ArticleDetail(props) {
{post?.type !== 'Page' && (<>
<Link href={`/archive#${post?.date?.start_date?.substr(0, 7)}`} passHref>
<div className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
<i className='far fa-calendar mr-1' /> {date}
<i className='far fa-calendar mr-1' /> {date}
</div>
</Link>
<span className='mr-2'> | <i className='far fa-calendar-check mr-2' />{post.lastEditedTime} </span>

View File

@@ -13,7 +13,7 @@ const BlogPostCard = ({ post, showSummary }) => {
const { locale } = useGlobal()
const showPreview = CONFIG_NEXT.POST_LIST_PREVIEW && post.blockMap
return (
<Card className="w-full animate__animated animate__fadeIn">
<Card className="w-full">
<div
key={post.id}
className="flex flex-col-reverse justify-between duration-300"

View File

@@ -1,5 +1,11 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
return <div
data-aos="fade-down"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className={className}>
<>{headerSlot}</>
<section className="shadow px-2 py-4 bg-white dark:bg-hexo-black-gray hover:shadow-xl duration-200">
{children}

View File

@@ -16,7 +16,13 @@ const PaginationNumber = ({ page, totalPage }) => {
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
return (
<div className="my-5 flex justify-center items-end font-medium text-black hover:shadow-xl duration-500 bg-white dark:bg-hexo-black-gray dark:text-gray-300 py-3 shadow space-x-2">
<div
data-aos="fade-down"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className="my-5 flex justify-center items-end font-medium text-black hover:shadow-xl duration-500 bg-white dark:bg-hexo-black-gray dark:text-gray-300 py-3 shadow space-x-2">
{/* 上一页 */}
<Link
href={{

View File

@@ -15,7 +15,13 @@ const PaginationSimple = ({ page, showNext }) => {
const router = useRouter()
const currentPage = +page
return (
<div className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
<div
data-aos="fade-down"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
<Link
href={{
pathname:

View File

@@ -24,49 +24,55 @@ const SideAreaLeft = props => {
const showToc = post && post.toc && post.toc.length > 1
return <aside id='left' className='hidden lg:block flex-col w-60 mr-4'>
<section className='w-60'>
{/* 菜单 */}
<section className='shadow hidden lg:block mb-5 pb-4 bg-white dark:bg-hexo-black-gray hover:shadow-xl duration-200'>
<Logo {...props} className='h-32' />
<div className='pt-2 px-2 font-sans'>
<MenuButtonGroup allowCollapse={true} {...props} />
</div>
{CONFIG_NEXT.MENU_SEARCH && <div className='px-2 pt-2 font-sans'>
<SearchInput {...props} />
</div>}
</section>
</section>
<section
data-aos="fade-down"
data-aos-duration="600"
data-aos-easing="ease-in-out"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className='w-60'>
{/* 菜单 */}
<section className='shadow hidden lg:block mb-5 pb-4 bg-white dark:bg-hexo-black-gray hover:shadow-xl duration-200'>
<Logo {...props} className='h-32' />
<div className='pt-2 px-2 font-sans'>
<MenuButtonGroup allowCollapse={true} {...props} />
</div>
{CONFIG_NEXT.MENU_SEARCH && <div className='px-2 pt-2 font-sans'>
<SearchInput {...props} />
</div>}
</section>
</section>
<div className='sticky top-4 hidden lg:block'>
<Card>
<Tabs>
{showToc && (
<div key={locale.COMMON.TABLE_OF_CONTENTS} className='dark:text-gray-400 text-gray-600 bg-white dark:bg-hexo-black-gray duration-200'>
<Toc toc={post.toc} />
</div>
)}
<div className='sticky top-4 hidden lg:block'>
<Card>
<Tabs>
{showToc && (
<div key={locale.COMMON.TABLE_OF_CONTENTS} className='dark:text-gray-400 text-gray-600 bg-white dark:bg-hexo-black-gray duration-200'>
<Toc toc={post.toc} />
</div>
)}
<div key={locale.NAV.ABOUT} className='mb-5 bg-white dark:bg-hexo-black-gray duration-200 py-6'>
<InfoCard {...props} />
<>
<div className='mt-2 text-center dark:text-gray-300 font-light text-xs'>
<span className='px-1 '>
<strong className='font-medium'>{postCount}</strong>{locale.COMMON.POSTS}</span>
<span className='px-1 busuanzi_container_site_uv hidden'>
| <strong className='pl-1 busuanzi_value_site_uv font-medium' />{locale.COMMON.VISITORS}</span>
{/* <span className='px-1 busuanzi_container_site_pv hidden'>
<div key={locale.NAV.ABOUT} className='mb-5 bg-white dark:bg-hexo-black-gray duration-200 py-6'>
<InfoCard {...props} />
<>
<div className='mt-2 text-center dark:text-gray-300 font-light text-xs'>
<span className='px-1 '>
<strong className='font-medium'>{postCount}</strong>{locale.COMMON.POSTS}</span>
<span className='px-1 busuanzi_container_site_uv hidden'>
| <strong className='pl-1 busuanzi_value_site_uv font-medium' />{locale.COMMON.VISITORS}</span>
{/* <span className='px-1 busuanzi_container_site_pv hidden'>
| <strong className='pl-1 busuanzi_value_site_pv font-medium'/>{locale.COMMON.VIEWS}</span> */}
</div>
</>
</div>
</Tabs>
</Card>
</div>
</>
</div>
</Tabs>
</Card>
{slot && <div className='flex justify-center'>
{slot}
</div>}
</div>
{slot && <div className='flex justify-center'>
{slot}
</div>}
</div>
</aside>
</aside>
}
export default SideAreaLeft

View File

@@ -1,8 +1,9 @@
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'
import Live2D from '@/components/Live2D'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -18,8 +19,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'
@@ -34,6 +35,11 @@ const LayoutBase = props => {
<div className='fixed right-4 bottom-4'>
<JumpToTopButton />
</div>
{/* 左下角悬浮 */}
<div className="bottom-4 -left-14 fixed justify-end z-40">
<Live2D />
</div>
</div>
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
const CONFIG_EMPTY = {
const CONFIG_NOBELIUM = {
TEST_CONFIG: 'TESET'
}
export default CONFIG_EMPTY
export default CONFIG_NOBELIUM

View File

@@ -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,