Merge pull request #36 from tangly1024/theme-hexo

Theme hexo
This commit is contained in:
tangly1024
2022-01-24 17:48:28 +08:00
committed by GitHub
57 changed files with 1727 additions and 81 deletions

View File

@@ -25,7 +25,13 @@ export async function getAllPosts ({ notionPageData, from, includePage = false }
const collectionQuery = notionPageData.collectionQuery
const data = []
if (!collectionQuery || collectionQuery.toString === '{}') {
console.warn('列表查询条件为空', notionPageData)
}
const pageIds = getAllPageIds(collectionQuery)
if (!pageIds || pageIds.length === 0) {
console.warn('页面ID列表为空')
}
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const properties = (await getPageProperties(id, pageBlock, schema)) || null
@@ -59,6 +65,9 @@ export async function getAllPosts ({ notionPageData, from, includePage = false }
}
})
if (!posts || posts.length === 0) {
console.warn('文章列表为空')
}
// Sort by date
if (BLOG.POSTS_SORT_BY === 'date') {
posts.sort((a, b) => {

View File

@@ -6,6 +6,6 @@ import { Layout404 } from '@/themes'
* @constructor
*/
export default function Custom404 () {
return <Layout404 />
export default function Custom404 (props) {
return <Layout404 {...props}/>
}

View File

@@ -11,7 +11,7 @@ import { LayoutSlug } from '@/themes'
*/
const About = (props) => {
if (!props.post) {
return <Custom404 />
return <Custom404 {...props} />
}
return <LayoutSlug {...props} />
}

View File

@@ -11,7 +11,7 @@ import Custom404 from '@/pages/404'
*/
const Slug = (props) => {
if (!props.post) {
return <Custom404 />
return <Custom404 {...props} />
}
return <LayoutSlug {...props} />
}

View File

@@ -6,7 +6,7 @@ import Custom404 from '@/pages/404'
const Page = (props) => {
if (!props?.meta) {
return <Custom404 />
return <Custom404 {...props} />
}
return <LayoutPage {...props} />
}

View File

@@ -16,7 +16,7 @@ export async function getStaticProps ({ params }) {
latestPosts
} = await getGlobalNotionData({
from,
includePage: true,
includePage: false,
tagsCount: 0
})
const filteredPosts = allPosts.filter(

BIN
public/bg_image.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

@@ -188,4 +188,8 @@ nav {
width: 100%;
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,#fff 70%);
padding-bottom: 34px;
}
.shadow-text{
text-shadow: 0.1em 0.1em 0.2em black;
}

View File

@@ -1,5 +1,6 @@
export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
return <div>
export const LayoutArchive = (props) => {
// const { posts, tags, categories, postCount } = props
return <div {...props}>
Archive Page
</div>
}

View File

@@ -2,39 +2,18 @@ import CommonHead from '@/components/CommonHead'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param children
* @param layout
* @param tags
* @param meta
* @param post
* @param currentSearch
* @param currentCategory
* @param currentTag
* @param categories
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = ({
children,
headerSlot,
tags,
meta,
post,
postCount,
sideBarSlot,
floatSlot,
rightAreaSlot,
currentSearch,
currentCategory,
currentTag,
categories
}) => {
return (<>
const LayoutBase = (props) => {
const { children, meta } = props
return <div>
<CommonHead meta={meta} />
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
{children}
</main>
</>)
</div>
}
export default LayoutBase

View File

@@ -1,5 +1,8 @@
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
return <div>
import LayoutBase from './LayoutBase'
export const LayoutCategory = (props) => {
const { category } = props
return <LayoutBase {...props}>
Category - {category}
</div>
</LayoutBase>
}

View File

@@ -1,11 +1,8 @@
export const LayoutCategoryIndex = ({
tags,
allPosts,
categories,
postCount,
latestPosts
}) => {
return <div>
import LayoutBase from './LayoutBase'
export const LayoutCategoryIndex = (props) => {
// const { tags, allPosts, categories, postCount, latestPosts } = props
return <LayoutBase {...props}>
CategoryIndex
</div>
</LayoutBase>
}

View File

@@ -1,3 +1,6 @@
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
return <div>Index</div>
import LayoutBase from './LayoutBase'
export const LayoutIndex = (props) => {
// const { posts, tags, meta, categories, postCount, latestPosts } = props
return <LayoutBase {...props}>Index</LayoutBase>
}

View File

@@ -1,5 +1,8 @@
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
return <div>
import LayoutBase from '../Hexo/LayoutBase'
export const LayoutPage = (props) => {
const { page } = props
return <LayoutBase {...props}>
Page - {page}
</div>
</LayoutBase>
}

View File

@@ -1,11 +1,8 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
export const LayoutSearch = ({
posts,
tags,
categories,
postCount
}) => {
export const LayoutSearch = (props) => {
const { posts } = props
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
@@ -20,9 +17,9 @@ export const LayoutSearch = ({
console.log(filteredPosts)
return <div>
return <LayoutBase {...props}>
Search {searchKey}
</div>
</LayoutBase>
}
function getSearchKey () {

View File

@@ -1,21 +1,45 @@
import BLOG from '@/blog.config'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import LayoutBase from './LayoutBase'
export const LayoutSlug = ({
post,
tags,
prev,
next,
recommendPosts,
categories,
postCount,
latestPosts
}) => {
return <div>
Slug
</div>
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
export const LayoutSlug = (props) => {
const { post } = props
const meta = {
title: `${post.title} | ${BLOG.TITLE}`,
description: post.summary,
type: 'article',
tags: post.tags
}
return <LayoutBase {...props} meta={meta}>
<h1>Slug - {post?.title}</h1>
<p>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
</section>
</p>
</LayoutBase>
}

View File

@@ -1,5 +1,8 @@
export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts }) => {
return <div>
import LayoutBase from './LayoutBase'
export const LayoutTag = (props) => {
const { tag } = props
return <LayoutBase>
Tag - {tag}
</div>
</LayoutBase>
}

View File

@@ -1,5 +1,8 @@
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
return <div>
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = (props) => {
// const { tags, categories, postCount, latestPosts } = props
return <LayoutBase {...props}>
TagIndex
</div>
</LayoutBase>
}

View File

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

39
themes/Hexo/Layout404.js Normal file
View File

@@ -0,0 +1,39 @@
import BLOG from '@/blog.config'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import LayoutBase from './LayoutBase'
export const Layout404 = props => {
const router = useRouter()
useEffect(() => {
// 延时3秒如果加载失败就返回首页
setTimeout(() => {
if (window) {
const article = document.getElementById('container')
if (!article) {
router.push('/').then(() => {
console.log('找不到页面', router.asPath)
})
}
}
}, 3000)
})
return (
<LayoutBase {...props} meta={{ title: `${BLOG.TITLE} | 页面找不到啦` }}>
<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">
<FontAwesomeIcon icon={faSpinner} spin={true} className="mr-2" />
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,64 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
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 { locale } = useGlobal()
// 深拷贝
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 meta = {
title: `${locale.NAV.ARCHIVE} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
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(() => {
if (window) {
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} meta={meta}>
<Card className='w-full'>
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 min-h-full">
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
</Card>
</LayoutBase>
}

69
themes/Hexo/LayoutBase.js Normal file
View File

@@ -0,0 +1,69 @@
import CommonHead from '@/components/CommonHead'
import { useEffect, useState } from 'react'
import Footer from './components/Footer'
import JumpToTopButton from './components/JumpToTopButton'
import SideRight from './components/SideRight'
import TopNav from './components/TopNav'
import smoothscroll from 'smoothscroll-polyfill'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param props
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = (props) => {
const { children, headerSlot, floatSlot, meta } = props
const [show, switchShow] = useState(false)
const [percent, changePercent] = useState(0) // 页面阅读百分比
const scrollListener = () => {
const targetRef = document.getElementById('wrapper')
const clientHeight = targetRef?.clientHeight
const scrollY = window.pageYOffset
const fullHeight = clientHeight - window.outerHeight
let per = parseFloat(((scrollY / fullHeight * 100)).toFixed(0))
if (per > 100) per = 100
const shouldShow = scrollY > 100 && per > 0
if (shouldShow !== show) {
switchShow(shouldShow)
}
changePercent(per)
}
useEffect(() => {
smoothscroll.polyfill()
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<div className='bg-white'>
<CommonHead meta={meta} />
<TopNav {...props}/>
{headerSlot}
<main id='wrapper' className='flex w-full justify-center py-8 min-h-screen'>
<div id='container-inner' className='w-full mx-auto flex justify-between max-w-6xl'>
{children}
<SideRight {...props}/>
</div>
</main>
{/* 右下角悬浮 */}
<div 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}/>
{floatSlot}
</div>
</div>
<Footer title={meta.title}/>
</div>)
}
export default LayoutBase

View File

@@ -0,0 +1,17 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import BlogPostListScroll from './components/BlogPostListScroll'
import LayoutBase from './LayoutBase'
export const LayoutCategory = (props) => {
const { tags, posts, category } = props
const { locale } = useGlobal()
const meta = {
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase {...props} meta={meta}>
<BlogPostListScroll posts={posts} tags={tags} currentCategory={category}/>
</LayoutBase>
}

View File

@@ -0,0 +1,43 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faFolder, faTh } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import Card from './components/Card'
import LayoutBase from './LayoutBase'
export const LayoutCategoryIndex = props => {
const { categories } = props
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.CATEGORY} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return (
<LayoutBase {...props} meta={meta}>
<Card className="bg-white dark:bg-gray-700 w-full min-h-screen">
<div className="dark:text-gray-200 mb-5 mx-3">
<FontAwesomeIcon icon={faTh} className="mr-4" />
{locale.COMMON.CATEGORY}:
</div>
<div id="category-list" className="duration-200 flex flex-wrap mx-8">
{Object.keys(categories).map(category => {
return (
<Link key={category} href={`/category/${category}`} passHref>
<div
className={
' duration-300 dark:hover:text-white rounded-lg px-5 cursor-pointer py-2 hover:bg-blue-600 hover:text-white'
}
>
<FontAwesomeIcon icon={faFolder} className="mr-4" />
{category}({categories[category]})
</div>
</Link>
)
})}
</div>
</Card>
</LayoutBase>
)
}

View File

@@ -0,0 +1,10 @@
import BlogPostListPage from './components/BlogPostListPage'
import Header from './components/Header'
import CONFIG_HEXO from './config_hexo'
import LayoutBase from './LayoutBase'
export const LayoutIndex = (props) => {
return <LayoutBase {...props} headerSlot={CONFIG_HEXO.HOME_BANNER_ENABLE && <Header/>}>
<BlogPostListPage {...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,38 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import BlogPostListPage from './components/BlogPostListPage'
import LayoutBase from './LayoutBase'
export const LayoutSearch = (props) => {
const { posts } = props
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = posts
}
const { locale } = useGlobal()
const meta = {
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.TITLE} `,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase {...props} meta={meta}>
<BlogPostListPage {...props} posts={filteredPosts}/>
</LayoutBase>
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

84
themes/Hexo/LayoutSlug.js Normal file
View File

@@ -0,0 +1,84 @@
import BLOG from '@/blog.config'
import formatDate from '@/lib/formatDate'
import { useGlobal } from '@/lib/global'
import { faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import CONFIG_NEXT from '../NEXT/config_next'
import ArticleDetail from './components/ArticleDetail'
import Card from './components/Card'
import LayoutBase from './LayoutBase'
export const LayoutSlug = props => {
const { post } = props
const meta = {
title: `${post.title} | ${BLOG.TITLE}`,
description: post.summary,
type: 'article',
tags: post.tags
}
const { locale } = useGlobal()
const date = formatDate(
post?.date?.start_date || post.createdTime,
locale.LOCALE
)
const headerSlot = (
<div className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url("/${CONFIG_NEXT.HOME_BANNER_IMAGE}")` }}>
<header className="animate__slideInDown animate__animated bg-black bg-opacity-50 absolute top-0 w-full h-96 py-10 flex justify-center items-center font-sans">
<div>
{/* 文章Title */}
<div className="font-bold text-3xl shadow-text flex justify-center text-white dark:text-white font-sans">
{post.title}
</div>
<section className="flex-wrap shadow-text flex justify-center mt-2 text-white dark:text-gray-400 font-light leading-8">
<div>
<Link href={`/category/${post.category}`} passHref>
<a className="cursor-pointer text-md mr-2 dark:hover:text-white border-b dark:border-gray-500 border-dashed">
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
{post.category}
</a>
</Link>
<span className="mr-2">|</span>
{post.type[0] !== 'Page' && (<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
>
<a className="pl-1 mr-2 cursor-pointer hover:underline border-b dark:border-gray-500 border-dashed">
{date}
</a>
</Link>
</>)}
<div className="hidden busuanzi_container_page_pv font-light mr-2">
<span className="mr-2">|</span>
<span className="mr-2 busuanzi_value_page_pv" />次访问
</div>
</div>
</section>
</div>
</header>
</div>
)
return (
<LayoutBase headerSlot={headerSlot} {...props} meta={meta}>
<Card className="w-full">
<ArticleDetail {...props} />
</Card>
</LayoutBase>
)
}

19
themes/Hexo/LayoutTag.js Normal file
View File

@@ -0,0 +1,19 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import BlogPostListScroll from './components/BlogPostListScroll'
import LayoutBase from './LayoutBase'
export const LayoutTag = (props) => {
const { tags, posts, tag } = props
const { locale } = useGlobal()
const meta = {
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return <LayoutBase {...props} meta={meta}>
<BlogPostListScroll posts={posts} tags={tags} currentTag={tag}/>
</LayoutBase>
}

View File

@@ -0,0 +1,36 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Card from './components/Card'
import TagItemMini from './components/TagItemMini'
import LayoutBase from './LayoutBase'
export const LayoutTagIndex = props => {
const { tags } = props
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.TAGS} | ${BLOG.TITLE}`,
description: BLOG.DESCRIPTION,
type: 'website'
}
return (
<LayoutBase {...props} meta={meta}>
<Card className='w-full'>
<div className="dark:text-gray-200 mb-5 ml-4">
<FontAwesomeIcon icon={faTag} className="mr-4" />
{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="px-2">
<TagItemMini key={tag.name} tag={tag} />
</div>
)
})}
</div>
</Card>
</LayoutBase>
)
}

View File

@@ -0,0 +1,98 @@
import Comment from '@/components/Comment'
import mediumZoom from 'medium-zoom'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { useEffect, useRef } from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
/**
*
* @param {*} param0
* @returns
*/
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
const zoom = typeof window !== 'undefined' && mediumZoom({
container: '.notion-viewport',
background: 'rgba(0, 0, 0, 0.2)',
margin: getMediumZoomMargin()
})
const zoomRef = useRef(zoom ? zoom.clone() : null)
useEffect(() => {
// 将所有container下的所有图片添加medium-zoom
const container = document?.getElementById('container')
const imgList = container?.getElementsByTagName('img')
if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
(zoomRef.current).attach(imgList[i])
}
}
})
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full md:px-5 ">
<article itemScope itemType="https://schema.org/Movie"
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
</section>
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
{/* 文章内嵌广告 */}
<ins className="adsbygoogle"
style={{ display: 'block', textAlign: 'center' }}
data-adtest="on"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-2708419466378217"
data-ad-slot="3806269138"/>
</section>
</article>
{/* 评论互动 */}
<div className="duration-200 px-12 w-screen md:w-full overflow-x-auto bg-white dark:bg-gray-800">
<div className='text-2xl mt-8 mx-8'>发表评论</div>
<Comment frontMatter={post} />
</div>
</div>)
}
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
function getMediumZoomMargin () {
const width = window.innerWidth
if (width < 500) {
return 8
} else if (width < 800) {
return 20
} else if (width < 1280) {
return 30
} else if (width < 1600) {
return 40
} else if (width < 1920) {
return 48
} else {
return 72
}
}

View File

@@ -0,0 +1,32 @@
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-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500'>
<div id={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span> &nbsp;
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className='dark:text-gray-400 dark:hover:text-gray-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,83 @@
import BLOG from '@/blog.config'
import { faFolder } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import TagItemMini from './TagItemMini'
import CONFIG_HEXO from '../config_hexo'
const BlogPostCard = ({ post, showSummary }) => {
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
return (
<div className='w-full shadow-xl hover:shadow-2xl border border-gray-100 rounded-xl bg-white dark:bg-gray-800 duration-300'>
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse lg:flex-row justify-between duration-300'>
<div className='lg:p-8 p-4 flex flex-col w-full'>
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={`cursor-pointer font-bold hover:underline text-3xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
{post.title}
</a>
</Link>
<div className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'} flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 `}>
<div>
<Link href={`/archive#${post?.date?.start_date?.substr(0, 7)}`} passHref>
<a className='font-light hover:underline cursor-pointer text-sm leading-4 mr-3'>{post.date.start_date}</a>
</Link>
</div>
</div>
{(!showPreview || showSummary) && <p className='mt-4 mb-24 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
{post.summary}
</p>}
{showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'>
<NotionRenderer
bodyClassName='max-h-full'
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
</div> }
<div className='text-gray-400 justify-between flex'>
<Link href={`/category/${post.category}`} passHref>
<a className='cursor-pointer font-light text-sm hover:underline transform'>
<FontAwesomeIcon icon={faFolder} className='mr-1' />{post.category}
</a>
</Link>
<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>
{CONFIG_HEXO.POST_LIST_COVER && post?.page_cover && (
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className='w-full relative duration-200 rounded-t-xl lg:rounded-r-xl lg:rounded-t-none cursor-pointer transform overflow-hidden'>
{/* eslint-disable-next-line @next/next/no-img-element */}
{/* <img src={post?.page_cover} alt={post.title} className='h-full object-cover'></img> */}
<Image className='hover:scale-125 rounded-t-xl lg:rounded-r-xl lg:rounded-t-none transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
</a>
</Link>
)}
</div >
</div>
)
}
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
export default BlogPostCard

View File

@@ -0,0 +1,12 @@
/**
* 空白博客 列表
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListEmpty = ({ currentSearch }) => {
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
</div>
}
export default BlogPostListEmpty

View File

@@ -0,0 +1,34 @@
import BlogPostCard from './BlogPostCard'
import PaginationNumber from './PaginationNumber'
import BLOG from '@/blog.config'
import BlogPostListEmpty from './BlogPostListEmpty'
/**
* 文章列表分页表格
* @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)
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />
} else {
return (
<div id="container" className='w-full'>
{/* 文章列表 */}
<div className="flex lg:space-y-4 space-y-1">
{posts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
<PaginationNumber page={page} totalPage={totalPage} />
</div>
)
}
}
export default BlogPostListPage

View File

@@ -0,0 +1,88 @@
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, { useCallback, useEffect, useRef, useState } from 'react'
import CONFIG_HEXO from '../config_hexo'
/**
* 博客列表滚动分页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HEXO.POST_LIST_SUMMARY }) => {
const postsPerPage = BLOG.POSTS_PER_PAGE
const [page, updatePage] = useState(1)
const postsToShow = getPostByPage(page, posts, postsPerPage)
let hasMore = false
if (posts) {
const totalCount = posts.length
hasMore = page * postsPerPage < totalCount
}
const handleGetMore = () => {
if (!hasMore) return
updatePage(page + 1)
}
// 监听滚动自动分页加载
const scrollTrigger = useCallback(throttle(() => {
const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
if (scrollS > clientHeight + 100) {
handleGetMore()
}
}, 500))
// 监听滚动
useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
})
const targetRef = 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'>
{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 glassmorphism shadow-xl rounded-xl dark:text-gray-200'
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`} </div>
</div>
</div>
}
}
/**
* 获取从第1页到指定页码的文章
* @param page 第几页
* @param totalPosts 所有文章
* @param postsPerPage 每页文章数量
* @returns {*}
*/
const getPostByPage = function (page, totalPosts, postsPerPage) {
return totalPosts.slice(
0,
postsPerPage * page
)
}
export default BlogPostListScroll

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
<>{headerSlot}</>
<section className="shadow-xl hover:shadow-2xl border border-gray-100 rounded-xl px-2 py-4 bg-white dark:bg-gray-800 duration-300">
{children}
</section>
</div>
}
export default Card

View File

@@ -0,0 +1,27 @@
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
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 font-sans mx-4'>
{Object.keys(categories).map(category => {
const selected = currentCategory === category
return <Link key={category} href={`/category/${category}`} passHref>
<a className={(selected
? 'hover:text-white dark:hover:text-white bg-blue-600 text-white '
: 'dark:text-gray-400 text-gray-500 hover:text-white dark:hover:text-white hover:bg-blue-600') +
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
<div> <FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={'mr-2'} />{category}({categories[category]})</div>
</a>
</Link>
})}
</div>
</>
}
export default CategoryGroup

View File

@@ -0,0 +1,38 @@
import React, { useEffect, useRef } from 'react'
const Collapse = props => {
const collapseRef = useRef(null)
const collapseSection = element => {
const sectionHeight = element.scrollHeight
requestAnimationFrame(function () {
element.style.height = sectionHeight + 'px'
requestAnimationFrame(function () {
element.style.height = 0 + 'px'
})
})
}
const expandSection = element => {
const sectionHeight = element.scrollHeight
element.style.height = sectionHeight + 'px'
const clearTime = setTimeout(() => {
element.style.height = 'auto'
}, 400)
clearTimeout(clearTime)
}
useEffect(() => {
const element = collapseRef.current
if (props.isOpen) {
expandSection(element)
} else {
collapseSection(element)
}
}, [props.isOpen])
return (
<div ref={collapseRef} style={{ height: '0px' }} className='overflow-hidden duration-200'>
{props.children}
</div>
)
}
Collapse.defaultProps = { isOpen: false }
export default Collapse

View File

@@ -0,0 +1,30 @@
import { faCopyright, faEye, faShieldAlt, faUsers, faHeart } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import BLOG from '@/blog.config'
const Footer = ({ title }) => {
const d = new Date()
const currentYear = d.getFullYear()
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
return (
<footer
className='dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-gray-400 text-sm p-6'
>
<FontAwesomeIcon icon={faCopyright} /> {`${startYear}${currentYear}`} <span><FontAwesomeIcon icon={faHeart} className='mx-1 animate-pulse'/> <a href={BLOG.LINK} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>.
<br/>
<span>Powered by <a href='https://notion.so' className='underline font-bold text-gray-500 dark:text-gray-300'>Notion</a> & <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span></span>
{BLOG.BEI_AN && <><br /><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
<span className='hidden busuanzi_container_site_pv'>
<FontAwesomeIcon icon={faEye}/><span className='px-1 busuanzi_value_site_pv'> </span> </span>
<span className='pl-2 hidden busuanzi_container_site_uv'>
<FontAwesomeIcon icon={faUsers}/> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
<br/>
<h1>{title}</h1>
</footer>
)
}
export default Footer

View File

@@ -0,0 +1,118 @@
import { useGlobal } from '@/lib/global'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useEffect, useState } from 'react'
import Typed from 'typed.js'
import CONFIG_HEXO from '../config_hexo'
let wrapperTop = 0
let windowTop = 0
let autoScroll = false
/**
*
* @returns 头图
*/
export default function Header () {
const [typed, changeType] = useState()
useEffect(() => {
if (!typed && window && document.getElementById('typed')) {
changeType(
new Typed('#typed', {
strings: CONFIG_HEXO.HOME_BANNER_GREETINGS,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
}
})
const { theme } = useGlobal()
const autoScrollEnd = () => {
if (autoScroll) {
windowTop = window.scrollY
autoScroll = false
}
}
const scrollTrigger = () => {
if (
(window.scrollY > windowTop) &
(window.scrollY < window.innerHeight) &&
!autoScroll
) {
autoScroll = true
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
setTimeout(autoScrollEnd, 500)
}
if (
(window.scrollY < windowTop) &
(window.scrollY < window.innerHeight) &&
!autoScroll
) {
autoScroll = true
window.scrollTo({ top: 0, behavior: 'smooth' })
setTimeout(autoScrollEnd, 500)
}
windowTop = window.scrollY
updateTopNav()
}
const updateTopNav = () => {
if (theme !== 'dark') {
const stickyNavElement = document.getElementById('sticky-nav')
if (window.scrollY < window.innerHeight) {
stickyNavElement?.classList?.add('dark')
} else {
stickyNavElement?.classList?.remove('dark')
}
}
}
function updateHeaderHeight () {
setTimeout(() => {
if (window) {
const wrapperElement = document.getElementById('wrapper')
wrapperTop = wrapperElement?.offsetTop
}
}, 500)
}
useEffect(() => {
updateHeaderHeight()
updateTopNav()
window.addEventListener('scroll', scrollTrigger)
window.addEventListener('resize', updateHeaderHeight)
return () => {
window.removeEventListener('scroll', scrollTrigger)
window.removeEventListener('resize', updateHeaderHeight)
}
})
return (
<header
id="header"
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black"
style={{
backgroundImage:
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${CONFIG_HEXO.HOME_BANNER_IMAGE}")`
}}
>
<div className="absolute flex h-full items-center lg:-mt-14 justify-center w-full text-4xl md:text-7xl text-white">
<div id='typed' className='flex text-center font-sans shadow-text'/>
</div>
<div
onClick={() => {
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
}}
className="cursor-pointer w-full text-center py-4 text-5xl absolute bottom-10 text-white"
>
<FontAwesomeIcon icon={faAngleDown} className='animate-bounce'/>
</div>
</header>
)
}

View File

@@ -0,0 +1,28 @@
import { useGlobal } from '@/lib/global'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import CONFIG_HEXO from '../config_hexo'
/**
* 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件
* @param targetRef 关联高度的目标html标签
* @param showPercent 是否显示百分比
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = ({ showPercent = true, percent }) => {
if (!CONFIG_HEXO.WIDGET_TO_TOP) {
return <></>
}
const { locale } = useGlobal()
return (<div className='flex space-x-1 items-center transform hover:scale-105 duration-200 py-2 px-3' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
<div className='dark:text-gray-200' title={locale.POST.TOP} >
<FontAwesomeIcon icon={faArrowUp} />
</div>
{showPercent && (<div className='text-xs dark:text-gray-200 block lg:hidden'>{percent}%</div>)}
</div>)
}
export default JumpToTopButton

View File

@@ -0,0 +1,42 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faArchive, faFileAlt } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import { useRouter } from 'next/router'
/**
* 最新文章列表
* @param posts 所有文章数据
* @param sliceCount 截取展示的数量 默认6
* @constructor
*/
const LatestPostsGroup = ({ posts }) => {
if (!posts) {
return <></>
}
// 获取当前路径
const currentPath = useRouter().asPath
const { locale } = useGlobal()
return <>
<div className='text-xs mb-2 px-1 flex flex-nowrap justify-between'>
<div className='font-light text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
</div>
{posts.map(post => {
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
return (
<Link key={post.id} title={post.title} href={`${BLOG.PATH}/article/${post.slug}`} passHref>
<a className={ 'my-1 mx-4 flex font-light'}>
<div className={ (selected ? 'text-white bg-blue-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-blue-600 px-2 duration-200 w-full ' +
'hover:text-white dark:hover:text-white cursor-pointer' }>
<FontAwesomeIcon icon={faFileAlt} className='mr-2'/>
<div className='truncate'>{post.title}</div>
</div>
</a>
</Link>
)
})}
</>
}
export default LatestPostsGroup

View File

@@ -0,0 +1,12 @@
import Link from 'next/link'
import BLOG from '@/blog.config'
import React from 'react'
const Logo = () => {
return <Link href='/' passHref>
<div className='flex flex-col justify-center items-center cursor-pointer bg-black space-y-3 font-bold'>
<div className='font-serif text-xl text-white'> {BLOG.TITLE}</div>
</div>
</Link>
}
export default Logo

View File

@@ -0,0 +1,41 @@
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArchive, faHome, faTag, faTh, faUser } from '@fortawesome/free-solid-svg-icons'
import CONFIG_HEXO from '../config_hexo'
const MenuButtonGroup = ({ postCount }) => {
const { locale } = useGlobal()
const router = useRouter()
const archiveSlot = <div className='bg-blue-300 dark:bg-blue-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
const links = [
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_HEXO.MENU_TAG },
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG_HEXO.MENU_ARCHIVE },
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_HEXO.MENU_ABOUT }
]
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans w-full'>
{links.map(link => {
if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
<a className={'py-1.5 my-1 px-5 duration-300 text-base justify-between hover:bg-blue-600 rounded-lg hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
(selected ? 'bg-blue-600 text-white' : ' ')} >
<div className='my-auto items-center justify-center flex '>
<FontAwesomeIcon icon={link.icon} />
<div className={'ml-4'}>{link.name}</div>
</div>
{link.slot}
</a>
</Link>
} else {
return null
}
})}
</nav>
}
export default MenuButtonGroup

View File

@@ -0,0 +1,94 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 数字翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const PaginationNumber = ({ page, totalPage }) => {
const router = useRouter()
const currentPage = +page
const showNext = page !== totalPage
const pages = generatePages(page, currentPage, totalPage)
return (
<div className='my-5 flex justify-center items-end font-medium text-black duration-500 bg-white dark:bg-blue-700 dark:text-gray-300 py-3 space-x-2'>
{/* 上一页 */}
<Link
href={ {
pathname: (currentPage - 1 === 1 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
} } passHref >
<div
rel='prev'
className={`${currentPage === 1 ? 'invisible' : 'block'} border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}
>
<FontAwesomeIcon icon={faAngleLeft}/>
</div>
</Link>
{pages}
{/* 下一页 */}
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
<div
rel='next'
className={`${+showNext ? 'block' : 'invisible'} border-t-2 border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}
>
<FontAwesomeIcon icon={faAngleRight}/>
</div>
</Link>
</div>
)
}
function getPageElement (page, currentPage) {
return <Link href={page === 1 ? '/' : `/page/${page}`} key={page} passHref>
<a className={(page + '' === currentPage + '' ? 'font-bold bg-blue-500 dark:bg-blue-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-blue-400 ') +
' border-white dark:border-blue-700 dark:hover:border-blue-400 cursor-pointer w-6 text-center font-light hover:font-bold'}>
{page}
</a>
</Link>
}
function generatePages (page, currentPage, totalPage) {
const pages = []
const groupCount = 7 // 最多显示页签数
if (totalPage <= groupCount) {
for (let i = 1; i <= totalPage; i++) {
pages.push(getPageElement(i, page))
}
} else {
pages.push(getPageElement(1, page))
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))
}
}
if (startPage + dynamicGroupCount < totalPage) {
pages.push(<div key={-2}>... </div>)
}
pages.push(getPageElement(totalPage, page))
}
return pages
}
export default PaginationNumber

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-50 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,68 @@
import { useRouter } from 'next/router'
import { useImperativeHandle, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
const [searchKey, setSearchKey] = useState(currentSearch || '')
const [onLoading, setLoadingState] = useState(false)
const router = useRouter()
const searchInputRef = useRef()
useImperativeHandle(cRef, () => {
return {
focus: () => {
searchInputRef?.current?.focus()
}
}
})
const handleSearch = (key) => {
if (key && key !== '') {
setLoadingState(true)
router.push({ pathname: '/search', query: { s: key } }).then(r => {
setLoadingState(false)
})
} 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 = ''
setSearchKey('')
}
const updateSearchKey = (val) => {
setSearchKey(val)
}
return <div className='flex'>
<input
ref={searchInputRef}
type='text'
className={'w-full rounded-lg text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
onKeyUp={handleKeyUp}
onChange={e => updateSearchKey(e.target.value)}
defaultValue={searchKey}
/>
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
onClick={() => { handleSearch(searchKey) }}>
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
</div>
{(searchKey && searchKey.length &&
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
</div>
)}
</div>
}
export default SearchInput

View File

@@ -0,0 +1,64 @@
import Router from 'next/router'
import Image from 'next/image'
import BLOG from '@/blog.config'
import Card from './Card'
import MenuButtonGroup from './MenuButtonGroup'
import SearchInput from './SearchInput'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChartArea, faTh } from '@fortawesome/free-solid-svg-icons'
import CategoryGroup from './CategoryGroup'
import LatestPostsGroup from './LatestPostsGroup'
import TagGroups from './TagGroups'
import SocialButton from './SocialButton'
export default function SideRight (props) {
const { postCount, currentCategory, categories, latestPosts, tags, currentTag } = props
return <div id='left' className='w-96 mx-4 space-y-4 hidden lg:block'>
<Card>
<div className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/') }}>
<Image
alt={BLOG.AUTHOR}
width={120}
height={120}
loading='lazy'
src='/avatar.jpg'
className='rounded-full'
/>
</div>
<div className='text-center text-xl pb-4'>{BLOG.TITLE}</div>
<SocialButton/>
</Card>
<Card>
<MenuButtonGroup/>
<SearchInput/>
</Card>
<Card>
<div className='text-xs font-light ml-2 mb-3 font-sans'>
<FontAwesomeIcon icon={faChartArea}/> 统计
</div>
<div className='text-xs font-sans font-light justify-center mx-6'>
<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>
<Card>
<div className='text-xs font-light ml-2 mb-1 font-sans'>
<FontAwesomeIcon icon={faTh}/> 分类
</div>
<CategoryGroup currentCategory={currentCategory} categories={categories}/>
</Card>
<Card>
<TagGroups tags={tags} currentTag={currentTag}/>
</Card>
<Card>
<LatestPostsGroup posts={latestPosts}/>
</Card>
</div>
}

View File

@@ -0,0 +1,36 @@
import BLOG from '@/blog.config'
import { faGithub, faTelegram, faTwitter, faWeibo } from '@fortawesome/free-brands-svg-icons'
import { faEnvelope, faRss } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
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-400 '>
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
<FontAwesomeIcon icon={faGithub} className='transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_TWITTER && <a target='_blank' rel='noreferrer' title={'twitter'} href={BLOG.CONTACT_TWITTER} >
<FontAwesomeIcon icon={faTwitter} className='transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_TELEGRAM && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_TELEGRAM} title={'telegram'} >
<FontAwesomeIcon icon={faTelegram} className='transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_WEIBO && <a target='_blank' rel='noreferrer' title={'weibo'} href={BLOG.CONTACT_WEIBO} >
<FontAwesomeIcon icon={faWeibo} className='transform hover:scale-125 duration-150'/>
</a>}
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
<FontAwesomeIcon icon={faEnvelope} className='transform hover:scale-125 duration-150'/>
</a>}
<a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
<FontAwesomeIcon icon={faRss} className='transform hover:scale-125 duration-150'/>
</a>
</div>
</div>
}
export default SocialButton

View File

@@ -0,0 +1,29 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
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'><FontAwesomeIcon icon={faTag} className='mr-1' />标签</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,17 @@
import { faTag } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
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 hover:bg-blue-500 hover:text-white duration-200
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
${selected
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-blue-900'
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-blue-800`}` }>
<div className='font-light dark:text-gray-400'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
</a>
</Link>
}
export default TagItemMini

View File

@@ -0,0 +1,123 @@
import { useGlobal } from '@/lib/global'
import { faAngleDoubleRight, faBars, faSearch, faTag, faThList, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import throttle from 'lodash.throttle'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import CategoryGroup from './CategoryGroup'
import Collapse from './Collapse'
import Logo from './Logo'
import MenuButtonGroup from './MenuButtonGroup'
import SearchDrawer from './SearchDrawer'
import TagGroups from './TagGroups'
import CONFIG_HEXO from '../config_hexo'
let windowTop = 0
/**
* 顶部导航
* @param {*} param0
* @returns
*/
const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => {
const { locale } = useGlobal()
const searchDrawer = useRef()
const scrollTrigger = useCallback(throttle(() => {
const scrollS = window.scrollY
if (scrollS >= windowTop && scrollS > 10) {
const nav = document.querySelector('#sticky-nav')
nav && nav.classList.replace('top-0', '-top-40')
windowTop = scrollS
} else {
const nav = document.querySelector('#sticky-nav')
nav && nav.classList.replace('-top-40', 'top-0')
windowTop = scrollS
}
}, 200), [])
// 监听滚动
useEffect(() => {
if (CONFIG_HEXO.NAV_TYPE === 'autoCollapse') {
scrollTrigger()
window.addEventListener('scroll', scrollTrigger)
}
return () => {
CONFIG_HEXO.NAV_TYPE === 'autoCollapse' && window.removeEventListener('scroll', scrollTrigger)
}
}, [])
const [isOpen, changeShow] = useState(false)
const toggleMenuOpen = () => {
changeShow(!isOpen)
}
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'><FontAwesomeIcon icon={faThList} className='mr-2' />{locale.COMMON.CATEGORY}</div>
<Link href={'/category'} passHref>
<a className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
</a>
</Link>
</div>
<CategoryGroup currentCategory={currentCategory} categories={categories} />
</section>
) }
{ tags && (
<section className='mt-4'>
<div className='text-sm py-2 px-2 flex flex-nowrap justify-between font-light dark:text-gray-200'>
<div className='text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faTag} className='mr-2'/>{locale.COMMON.TAGS}</div>
<Link href={'/tag'} passHref>
<a className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'>
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
</a>
</Link>
</div>
<div className='p-2'>
<TagGroups tags={tags} currentTag={currentTag} />
</div>
</section>
) }
</>
return (<div id='top-nav' className='z-40 block lg:hidden'>
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
{/* 导航栏 */}
<div id='sticky-nav' className={`${CONFIG_HEXO.NAV_TYPE !== 'normal' ? 'fixed' : ''} lg:relative w-full top-0 z-20 transform duration-500`}>
<div className='w-full flex justify-between items-center p-4 bg-black text-white'>
{/* 左侧LOGO 标题 */}
<div className='flex flex-none flex-grow-0'>
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
{ isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
</div>
</div>
<div className='flex'>
<Logo/>
</div>
{/* 右侧功能 */}
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<div className="cursor-pointer block lg:hidden" onClick={() => { searchDrawer?.current?.show() }}>
<FontAwesomeIcon icon={faSearch} className="mr-2" />{locale.NAV.SEARCH}
</div>
</div>
</div>
<Collapse isOpen={isOpen}>
<div className='bg-white py-1 px-5'>
<MenuButtonGroup postCount={postCount}/>
</div>
</Collapse>
</div>
</div>)
}
export default TopNav

View File

@@ -0,0 +1,18 @@
const CONFIG_HEXO = {
HOME_BANNER_ENABLE: true,
HOME_BANNER_GREETINGS: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
HOME_BANNER_IMAGE: './bg_image.jpg', // see /public/bg_image.jpg
// 菜单
MENU_ABOUT: false, // 显示关于
MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true, // 显示搜索
POST_LIST_COVER: true, // 文章封面
POST_LIST_SUMMARY: true, // 文章摘要
NAV_TYPE: 'autoCollapse', // ['fixed','autoCollapse','normal'] 分别是固定屏幕顶部、屏幕顶部自动折叠,不固定
WIDGET_TO_TOP: true
}
export default CONFIG_HEXO

10
themes/Hexo/index.js Normal file
View File

@@ -0,0 +1,10 @@
export { LayoutIndex } from './LayoutIndex'
export { LayoutSearch } from './LayoutSearch'
export { LayoutArchive } from './LayoutArchive'
export { LayoutSlug } from './LayoutSlug'
export { Layout404 } from './Layout404'
export { LayoutCategory } from './LayoutCategory'
export { LayoutCategoryIndex } from './LayoutCategoryIndex'
export { LayoutPage } from './LayoutPage'
export { LayoutTag } from './LayoutTag'
export { LayoutTagIndex } from './LayoutTagIndex'

View File

@@ -18,7 +18,7 @@ export const Layout404 = () => {
})
}
}
}, 30000000)
}, 3000)
})
return <LayoutBase meta={{ title: `${BLOG.TITLE} | 页面找不到啦` }}>

View File

@@ -2,6 +2,7 @@
* 修改 from 后面的路径,实现主题切换
*/
export * from './NEXT' // 切换主题
// export * from './Empty' // 主题
// export * from './NEXT'
// export * from './Fukasawa'
// export * from './Empty'
export * from './Hexo' //