mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-17 23:16:50 +00:00
feature: 剥离主题目录
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 统计网站信息
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function Analytics ({ postCount }) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <>
|
||||
{/* <div className='px-5 text-sm font-light pb-1 text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faChartBar} className='mr-2' />{locale.COMMON.ANALYTICS}</div> */}
|
||||
<div className='mt-2 text-center dark:text-gray-300 font-light text-xs'>
|
||||
<span className='px-1 '>
|
||||
<strong className='font-medium'>{postCount}</strong>{locale.COMMON.POSTS}</span>
|
||||
<span className='px-1 busuanzi_container_site_uv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_uv font-medium'></strong>{locale.COMMON.VISITORS}</span>
|
||||
{/* <span className='px-1 busuanzi_container_site_pv hidden'>
|
||||
| <strong className='pl-1 busuanzi_value_site_pv font-medium'></strong>{locale.COMMON.VIEWS}</span> */}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ArticleCopyright ({ author, url }) {
|
||||
if (!BLOG.widget?.showCopyRight) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return <section className="dark:text-gray-300 mt-6">
|
||||
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-700 bg-gray-100 p-5 leading-8 border-l-2 border-blue-500">
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
|
||||
<Link href="/about">
|
||||
<a className="hover:underline">{author}</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.URL}:</strong>
|
||||
<a className="hover:underline" href={url}>
|
||||
{url}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
|
||||
{locale.COMMON.COPYRIGHT_NOTICE}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogAround from '@/components/BlogAround'
|
||||
import Comment from '@/components/Comment'
|
||||
import RecommendPosts from '@/components/RecommendPosts'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import TagItem from '@/components/TagItem'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
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'
|
||||
import ArticleCopyright from './ArticleCopyright'
|
||||
import WordCount from './WordCount'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
const url = BLOG.link + useRouter().asPath
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
|
||||
|
||||
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="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
|
||||
<header className='animate__slideInDown animate__animated'>
|
||||
{post.type && !post.type.includes('Page') && post?.page_cover && (
|
||||
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
|
||||
{/* <div className="w-full h-60 relative lg:h-96 transform duration-200 md:flex-shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={post?.page_cover}
|
||||
loading="eager"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
alt={post.title}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
|
||||
{post.type[0] !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
|
||||
{date}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
</>)}
|
||||
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<FontAwesomeIcon icon={faEye} className='mr-1'/>
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv"
|
||||
></span>
|
||||
<span className='mr-2'>|</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-nowrap whitespace-nowrap items-center font-light text-md'>
|
||||
<WordCount/>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{/* <hr className="mt-2" /> */}
|
||||
|
||||
</header>
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-1'>
|
||||
{post.blockMap && (
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138"></ins>
|
||||
</section>
|
||||
|
||||
{/* 版权声明 */}
|
||||
<ArticleCopyright author={BLOG.author} url={url} />
|
||||
|
||||
{/* 推荐文章 */}
|
||||
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
|
||||
|
||||
{/* 标签列表 */}
|
||||
<section className="md:flex md:justify-between">
|
||||
{post.tagItems && (
|
||||
<div className="flex flex-nowrap leading-8 p-1 py-4 overflow-x-auto">
|
||||
<div className="hidden md:block dark:text-gray-300 whitespace-nowrap">
|
||||
{locale.COMMON.TAGS}:
|
||||
</div>
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItem key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ShareBar post={post} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BlogAround prev={prev} next={next} />
|
||||
|
||||
</article>
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="duration-200 shadow w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 上一篇,下一篇文章
|
||||
* @param {prev,next} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlogAround ({ prev, next }) {
|
||||
if (!prev || !next) {
|
||||
return <></>
|
||||
}
|
||||
return <section className='text-gray-800 border-t dark:text-gray-300 flex flex-wrap lg:flex-nowrap lg:space-x-10 justify-between py-2'>
|
||||
<Link href={`/article/${prev.slug}`} passHref>
|
||||
<a className='text-sm py-3 text-gray-400 hover:underline cursor-pointer'>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/article/${next.slug}`} passHref>
|
||||
<a className='text-sm flex py-3 text-gray-400 hover:underline cursor-pointer'>{next.title}
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
/**
|
||||
* 博客归档列表
|
||||
* @param posts 所有文章
|
||||
* @param archiveTitle 归档标题
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
|
||||
const targetRef = useRef(null)
|
||||
if (!posts || posts.length === 0) {
|
||||
return <></>
|
||||
} else {
|
||||
return <div ref={targetRef}>
|
||||
<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 name={post?.date?.start_date}><span className='text-gray-400'>{post.date.start_date}</span>
|
||||
<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
|
||||
@@ -1,87 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faAngleRight, faFolder } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import Card from './Card'
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const showPreview = BLOG.home?.showPreview && post.blockMap
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse 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={`/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>
|
||||
<span className='mx-2'>|</span>
|
||||
<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 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>
|
||||
|
||||
{(!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='article-cover pointer-events-none'>
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<a className='hover:bg-opacity-100 hover:scale-105 pointer-events-auto transform duration-300 rounded-md p-2 text-red-500 cursor-pointer'>
|
||||
{locale.COMMON.ARTICLE_DETAIL}
|
||||
<FontAwesomeIcon className='ml-1' icon={faAngleRight} /></a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{BLOG.home?.showPostCover && post?.page_cover && (
|
||||
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
|
||||
<div className='h-72 w-full relative duration-200 cursor-pointer transform overflow-hidden'>
|
||||
<Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div >
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
export default BlogPostCard
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
/**
|
||||
* 空白博客 列表
|
||||
* @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
|
||||
@@ -1,34 +0,0 @@
|
||||
import BlogPostCard from '@/components/BlogPostCard'
|
||||
import PaginationNumber from './PaginationNumber'
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListEmpty from '@/components/BlogPostListEmpty'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
* @param page 当前页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container">
|
||||
{/* 文章列表 */}
|
||||
<div className="flex flex-wrap 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
|
||||
@@ -1,88 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostCard from '@/components/BlogPostCard'
|
||||
import BlogPostListEmpty from '@/components/BlogPostListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 博客列表滚动分页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @param targetRef 指向父容器,用于计算下拉滚动的高度
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home.showSummary }) => {
|
||||
const postsPerPage = BLOG.postsPerPage
|
||||
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}>
|
||||
|
||||
{/* 文章列表 */}
|
||||
<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
|
||||
@@ -1,9 +0,0 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow px-2 py-4 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
export default Card
|
||||
@@ -1,24 +0,0 @@
|
||||
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 }) => {
|
||||
return <>
|
||||
<div id='category-list' className='dark:border-gray-600 flex flex-wrap'>
|
||||
{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-gray-600 text-white '
|
||||
: 'dark:text-gray-400 text-gray-500 hover:text-white hover:bg-gray-500 dark:hover:text-white') +
|
||||
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
|
||||
<FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={`${selected ? 'text-white' : 'text-gray-400'} mr-2`} />{category}({categories[category]})
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default CategoryGroup
|
||||
@@ -1,36 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const CategoryList = ({ currentCategory, categories }) => {
|
||||
if (!categories) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <ul className='flex py-1 space-x-3'>
|
||||
<li className='w-16 py-2 dark:text-gray-200 whitespace-nowrap'>{locale.COMMON.CATEGORY}</li>
|
||||
{Object.keys(categories).map(category => {
|
||||
const selected = category === currentCategory
|
||||
return (
|
||||
<Link key={category} href={`/category/${category}`} passHref>
|
||||
<li
|
||||
className={`cursor-pointer border rounded-xl duration-200 mr-1 my-1 px-2 py-1 font-light text-sm whitespace-nowrap dark:text-gray-300
|
||||
${selected
|
||||
? 'text-white bg-gray-500 dark:hover:bg-gray-900 dark:bg-gray-500 dark:border-gray-800'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-300 dark:hover:bg-gray-700 dark:bg-gray-600 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<a>
|
||||
<FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className='mr-1' />
|
||||
{`${category} `}
|
||||
</a>
|
||||
</li>
|
||||
</Link>)
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
|
||||
export default CategoryList
|
||||
@@ -1,38 +0,0 @@
|
||||
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
|
||||
@@ -5,7 +5,7 @@ import BLOG from '@/blog.config'
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ThirdPartyScript = () => {
|
||||
const CommonScript = () => {
|
||||
return (<>
|
||||
{BLOG.comment?.DaoVoiceId && (<>
|
||||
{/* DaoVoice 反馈 */}
|
||||
@@ -109,4 +109,4 @@ const ThirdPartyScript = () => {
|
||||
</>)
|
||||
}
|
||||
|
||||
export default ThirdPartyScript
|
||||
export default CommonScript
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faInfo } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 悬浮在屏幕右下角,联系我的按钮
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ContactButton = () => {
|
||||
return (
|
||||
<Link href='/about'>
|
||||
<a className={'fixed right-10 bottom-40 animate__fadeInRight animate__animated animate__faster'}>
|
||||
<span
|
||||
className='dark:bg-black bg-white px-5 py-3 cursor-pointer shadow-card text-xl hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow'>
|
||||
<FontAwesomeIcon icon={faInfo} className='dark:text-gray-200 ' title='about' />
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactButton
|
||||
@@ -1,20 +0,0 @@
|
||||
import { loadUserThemeFromCookies, saveTheme, useGlobal } from '@/lib/global'
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
const DarkModeButton = () => {
|
||||
const { changeTheme } = useGlobal()
|
||||
const userTheme = loadUserThemeFromCookies()
|
||||
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newTheme = (userTheme === 'light' ? 'dark' : 'light')
|
||||
saveTheme(newTheme)
|
||||
changeTheme(newTheme)
|
||||
}
|
||||
return <div className='z-10 duration-200 text-xs cursor-pointer py-1.5 px-1'>
|
||||
<FontAwesomeIcon icon={userTheme === 'dark' ? faSun : faMoon} id='darkModeButton' className='hover:scale-125 transform duration-200'
|
||||
onClick={handleChangeDarkMode} />
|
||||
</div>
|
||||
}
|
||||
export default DarkModeButton
|
||||
@@ -1,37 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export default function FloatDarkModeButton () {
|
||||
if (!BLOG.widget?.showDarkMode) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const { changeTheme } = useGlobal()
|
||||
const userTheme = loadUserThemeFromCookies()
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newTheme = userTheme === 'light' ? 'dark' : 'light'
|
||||
saveTheme(newTheme)
|
||||
changeTheme(newTheme)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove(userTheme)
|
||||
htmlElement.classList?.add(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleChangeDarkMode}
|
||||
className={ ' text-black dark:border-gray-500 flex justify-center items-center dark:text-gray-200 py-2 px-3'
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={userTheme === 'dark' ? faSun : faMoon}
|
||||
id="darkModeButton"
|
||||
className="hover:scale-150 transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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>
|
||||
<br />
|
||||
|
||||
<FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2 font-bold'>闽ICP备20010331号</a>
|
||||
<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
|
||||
@@ -1,118 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
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'
|
||||
|
||||
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: BLOG.home.homeBannerStrings,
|
||||
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("${BLOG.home.homeBannerImage}")`
|
||||
}}
|
||||
>
|
||||
<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-serif'/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Image from 'next/image'
|
||||
import Router from 'next/router'
|
||||
import React from 'react'
|
||||
import SocialButton from './SocialButton'
|
||||
|
||||
const InfoCard = ({ postCount }) => {
|
||||
return <>
|
||||
<div className='flex flex-col items-center justify-center '>
|
||||
<div className='hover:rotate-45 hover:scale-125 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/about') }}>
|
||||
<Image
|
||||
alt={BLOG.author}
|
||||
width={120}
|
||||
height={120}
|
||||
loading='lazy'
|
||||
src='/avatar.jpg'
|
||||
className='rounded-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-2xl font-serif dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.author}</div>
|
||||
<div className='font-light dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.bio}</div>
|
||||
<SocialButton/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default InfoCard
|
||||
@@ -1,56 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param targetRef 关联高度的目标html标签
|
||||
* @param showPercent 是否显示百分比
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
if (!BLOG.widget?.showToBottom) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function scrollToBottom () {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
window.scrollTo({ top: targetRef.clientHeight, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<div className='flex space-x-1 transform hover:scale-105 duration-200 py-2 px-3' onClick={scrollToBottom} >
|
||||
<div className='dark:text-gray-200' >
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</div>
|
||||
{showPercent && (<div className='dark:text-gray-200 block lg:hidden'>{percent}%</div>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default JumpToBottomButton
|
||||
@@ -1,28 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param targetRef 关联高度的目标html标签
|
||||
* @param showPercent 是否显示百分比
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
if (!BLOG.widget?.showToTop) {
|
||||
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
|
||||
@@ -1,42 +0,0 @@
|
||||
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-sm pb-1 px-2 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 flex font-light'}>
|
||||
<div className={ (selected ? 'text-white bg-gray-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-gray-500 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
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 左上角悬浮菜单栏
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LeftFloatButton = () => {
|
||||
// 监听resize事件
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', collapseSideBar)
|
||||
collapseSideBar()
|
||||
return () => {
|
||||
window.removeEventListener('resize', collapseSideBar)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const collapseSideBar = throttle(() => {
|
||||
if (window.innerWidth > 1300) {
|
||||
changeCollapse(false)
|
||||
} else {
|
||||
changeCollapse(true)
|
||||
}
|
||||
}, 500)
|
||||
const [collapse, changeCollapse] = useState(true)
|
||||
return <div
|
||||
className={(collapse ? 'left-0' : 'left-72') + ' z-30 fixed flex flex-nowrap md:flex-col top-0 pl-4 py-1 duration-500 ease-in-out'}>
|
||||
{/* 菜单折叠 */}
|
||||
<div className='p-1 border hover:shadow-xl duration-200 dark:border-gray-500 h-12 bg-white dark:bg-gray-600 dark:bg-opacity-70 bg-opacity-70
|
||||
dark:hover:bg-gray-100 text-xl cursor-pointer mr-2 my-2 dark:text-gray-300 dark:hover:text-black'>
|
||||
<FontAwesomeIcon icon={faBars} className='p-2.5 hover:scale-125 transform duration-200'
|
||||
onClick={() => changeCollapse(!collapse)} />
|
||||
</div>
|
||||
{/* 夜间模式 */}
|
||||
<DarkModeButton />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default LeftFloatButton
|
||||
@@ -1,52 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
let hasLoad = false
|
||||
export default function Live2D () {
|
||||
if (!BLOG.widget?.showPet) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !hasLoad) {
|
||||
initLive2D()
|
||||
hasLoad = true
|
||||
}
|
||||
|
||||
return <div className='fixed right-0 bottom-0 hidden md:block lg:mr-24 2xl:mr-40 z-20'>
|
||||
<canvas id="live2d"className='animate__slideInLeft animate__animated' width="280" height="250"></canvas>
|
||||
</div>
|
||||
}
|
||||
|
||||
function initLive2D () {
|
||||
// 加载 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(() => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.widget.petLink)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 封装异步加载资源的方法
|
||||
function loadExternalResource (url, type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tag
|
||||
|
||||
if (type === 'css') {
|
||||
tag = document.createElement('link')
|
||||
tag.rel = 'stylesheet'
|
||||
tag.href = url
|
||||
} else if (type === 'js') {
|
||||
tag = document.createElement('script')
|
||||
tag.src = url
|
||||
}
|
||||
if (tag) {
|
||||
tag.onload = () => resolve(url)
|
||||
tag.onerror = () => reject(url)
|
||||
document.head.appendChild(tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import Head from 'next/head'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Live2DWife () {
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
initLive2DWife()
|
||||
}
|
||||
})
|
||||
return <>
|
||||
<Head><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css"/></Head>
|
||||
</>
|
||||
}
|
||||
|
||||
function initLive2DWife () {
|
||||
// 注意:live2d_path 参数应使用绝对路径
|
||||
const live2dPath = 'https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/'
|
||||
// const live2d_path = "/live2d-widget/";
|
||||
|
||||
// 封装异步加载资源的方法
|
||||
function loadExternalResource (url, type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tag
|
||||
|
||||
if (type === 'css') {
|
||||
tag = document.createElement('link')
|
||||
tag.rel = 'stylesheet'
|
||||
tag.href = url
|
||||
} else if (type === 'js') {
|
||||
tag = document.createElement('script')
|
||||
tag.src = url
|
||||
}
|
||||
if (tag) {
|
||||
tag.onload = () => resolve(url)
|
||||
tag.onerror = () => reject(url)
|
||||
document.head.appendChild(tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载 waifu.css live2d.min.js waifu-tips.js
|
||||
if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
loadExternalResource(live2dPath + 'waifu.css', 'css'),
|
||||
loadExternalResource(live2dPath + 'live2d.min.js', 'js'),
|
||||
loadExternalResource(live2dPath + 'waifu-tips.js', 'js')
|
||||
]).then(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
initWidget({
|
||||
waifuPath: live2dPath + 'waifu-tips.json',
|
||||
// apiPath: "https://live2d.fghrsh.net/api/",
|
||||
cdnPath: 'https://cdn.jsdelivr.net/gh/fghrsh/live2d_api/'
|
||||
})
|
||||
})
|
||||
}
|
||||
// initWidget 第一个参数为 waifu-tips.json 的路径,第二个参数为 API 地址
|
||||
// API 后端可自行搭建,参考 https://github.com/fghrsh/live2d_api
|
||||
// 初始化看板娘会自动加载指定目录下的 waifu-tips.json
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
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-10 w-full h-screen container mx-auto'}>
|
||||
<div className="mx-auto">
|
||||
<FontAwesomeIcon icon={faSpinner} spin size='2x'/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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 h-32 font-bold'>
|
||||
<div className='font-serif text-xl text-white'> {BLOG.title}</div>
|
||||
<div className='text-sm text-gray-300 font-light'> {BLOG.description}</div>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
export default Logo
|
||||
@@ -1,41 +0,0 @@
|
||||
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 BLOG from 'blog.config'
|
||||
|
||||
const MenuButtonGroup = ({ allowCollapse = false, postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const archiveSlot = <div className='bg-gray-300 dark:bg-gray-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
|
||||
|
||||
const links = [
|
||||
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.menu.showCategory },
|
||||
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.menu.showTag },
|
||||
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: BLOG.menu.showArchive },
|
||||
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: BLOG.menu.showAbout }
|
||||
]
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
||||
<a className={'py-1.5 px-5 duration-300 text-base justify-between hover:bg-gray-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
|
||||
(selected ? 'bg-gray-200 text-black' : ' ')} >
|
||||
<div className='my-auto items-center justify-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
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import Image from 'next/image'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const NavBar = () => {
|
||||
const links = []
|
||||
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.icon && (<i className={'px-1 fa ' + link.icon} />))} {link.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ navBarTitle, fullWidth = true }) => {
|
||||
const navRef = useRef(null)
|
||||
const sentinelRef = useRef([])
|
||||
// 当Header移出屏幕时改变的样式
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current.classList.remove('sticky-nav-full')
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(handler)
|
||||
observer.observe(sentinelRef.current)
|
||||
// Don't touch this, I have no idea how it works XD
|
||||
// return () => {
|
||||
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
// }
|
||||
}, [sentinelRef])
|
||||
return (
|
||||
<>
|
||||
{BLOG.autoCollapsedNavBar === true && (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var windowTop=0;
|
||||
function scrollTrigger(){
|
||||
let scrollS = window.scrollY;
|
||||
let nav = document.querySelector('.sticky-nav');
|
||||
if(scrollS >= windowTop){
|
||||
nav.style.opacity = 0;
|
||||
windowTop = scrollS;
|
||||
}else{
|
||||
nav.style.opacity = 1;
|
||||
windowTop = scrollS;
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll',scrollTrigger);
|
||||
`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className='observer-element h-0.5 ' ref={sentinelRef} />
|
||||
|
||||
<div
|
||||
className='z-30 px-4 sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center py-6 bg-white bg-opacity-80 '
|
||||
id='sticky-nav'
|
||||
ref={navRef}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
|
||||
<div className='flex cursor-pointer'>
|
||||
<div className='px-2 text-xl'>
|
||||
<FontAwesomeIcon icon={faBars} className='hover:scale-125 transform duration-200' />
|
||||
</div>
|
||||
<Image
|
||||
alt={BLOG.title}
|
||||
width={28}
|
||||
height={28}
|
||||
src='/avatar.jpg'
|
||||
className='rounded-full'
|
||||
/>
|
||||
<div
|
||||
className='mx-1 text-center cursor-pointer text-xl p-1
|
||||
dark:bg-gray-900 dark:text-gray-300 font-semibold
|
||||
dark:hover:bg-gray-600 text-black hover:scale-105
|
||||
hover:shadow-2xl duration-200 transform'>{BLOG.title}</div>
|
||||
|
||||
</div>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className='ml-1 font-medium text-gray-500 dark:text-night header-name'>
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className='ml-1 font-medium dark:text-night header-name'>
|
||||
<span className='font-normal'>{BLOG.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,94 +0,0 @@
|
||||
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 hover:shadow-xl duration-500 bg-white dark:bg-gray-700 dark:text-gray-300 py-3 shadow 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-gray-700 hover:border-gray-400 dark:hover:border-gray-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-gray-700 hover:border-gray-400 dark:hover:border-gray-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-gray-500 dark:bg-gray-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-gray-400 ') +
|
||||
' border-white dark:border-gray-700 dark:hover:border-gray-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
|
||||
@@ -1,42 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 简易翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationSimple = ({ page, showNext }) => {
|
||||
const { locale } = useGlobal()
|
||||
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'>
|
||||
<Link
|
||||
href={ {
|
||||
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
|
||||
} } passHref >
|
||||
<button
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'} w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
||||
<button
|
||||
rel='next'
|
||||
className={`${+showNext ? 'block' : 'invisible'} w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationSimple
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 顶部页面阅读进度条
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || 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 font-sans">
|
||||
<div
|
||||
className="h-4 bg-gray-600 duration-200"
|
||||
style={{ width: `${percent}%` }}
|
||||
>
|
||||
{showPercent && (
|
||||
<div className="text-right text-white text-xs">{percent}%</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Progress
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 展示文章推荐
|
||||
*/
|
||||
const RecommendPosts = ({ recommendPosts }) => {
|
||||
if (!BLOG.widget?.showRelatePosts || !recommendPosts || recommendPosts.length < 1) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return (
|
||||
<div className="pt-2 border pl-4 py-2 my-4 dark:text-gray-300 ">
|
||||
<div className="mb-2 font-bold text-lg">{locale.COMMON.RELATE_POSTS} :</div>
|
||||
<ul className="font-light text-sm">
|
||||
{recommendPosts.map(post => (
|
||||
<li className="py-1" key={post.id}>
|
||||
<Link href={`/article/${post.slug}`}>
|
||||
<a className="cursor-pointer hover:underline">
|
||||
{post.title}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RecommendPosts
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faQrcode } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 赞赏按钮
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const RewardButton = () => {
|
||||
const openPopover = () => {
|
||||
if (window) {
|
||||
document.getElementById('reward-qrcode').classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
const closePopover = () => {
|
||||
if (window) {
|
||||
document.getElementById('reward-qrcode').classList.add('hidden')
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className='justify-center'>
|
||||
<div onMouseEnter={openPopover} onMouseLeave={closePopover}
|
||||
className='bg-pink-500 py-2 w-36 mx-auto animate__jello text-white hover:bg-green-400 duration-200 transform hover:scale-110 px-3 rounded cursor-pointer'>
|
||||
<FontAwesomeIcon icon={faQrcode} className='mr-2' />
|
||||
<span>打赏一杯咖啡</span>
|
||||
</div>
|
||||
|
||||
<div onMouseEnter={openPopover} onMouseLeave={closePopover} id='reward-qrcode' className='hidden flex space-x-10 animate__animated animate__fadeIn duration-200 my-5 px-5 mx-auto py-6 justify-center bg-white dark:bg-black dark:text-gray-200'>
|
||||
<div className='w-80'><Image width='auto' height='auto' layout='responsive' objectFit='fill' src='/reward_code_alipay.png' /></div>
|
||||
<div className='w-80'><Image width='auto' height='auto' layout='responsive' objectFit='fill' src='/reward_code_wechat.png' /></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RewardButton
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -1,66 +0,0 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
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 { locale } = useGlobal()
|
||||
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 border dark:border-gray-600 w-full bg-gray-100 dark:bg-gray-900'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 border-gray-300 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={searchKey}
|
||||
/>
|
||||
{(searchKey && searchKey.length && <FontAwesomeIcon icon={faTimes} className='text-gray-300 float-right m-3 cursor-pointer' onClick={cleanSearch} />)}
|
||||
|
||||
<div className='p-3 bg-gray-50 flex border-l dark:border-gray-700 dark:hover:bg-gray-800 dark:bg-gray-600 justify-center items-center cursor-pointer'
|
||||
onClick={() => { handleSearch(searchKey) }}>
|
||||
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:scale-125 hover:text-black transform duration-200 dark:text-gray-300 dark:hover:text-white text-gray-600 cursor-pointer ' />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
@@ -1,97 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import QRCode from 'qrcode.react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faFacebookSquare,
|
||||
faQq,
|
||||
faTelegram,
|
||||
faTwitterSquare,
|
||||
faWeibo,
|
||||
faWeixin
|
||||
} from '@fortawesome/free-brands-svg-icons'
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const ShareBar = ({ post }) => {
|
||||
if (!BLOG.widget?.showShareBar) {
|
||||
return <></>
|
||||
}
|
||||
const router = useRouter()
|
||||
const shareUrl = BLOG.link + router.asPath
|
||||
|
||||
// 二维码悬浮
|
||||
const [qrCodeShow, setQrCodeShow] = React.useState(false)
|
||||
const btnRef = React.createRef()
|
||||
const popoverRef = React.createRef()
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const openPopover = () => {
|
||||
createPopper(btnRef.current, popoverRef.current, {
|
||||
placement: 'top'
|
||||
})
|
||||
setQrCodeShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setQrCodeShow(false)
|
||||
}
|
||||
|
||||
const copyUrl = () => {
|
||||
copy(shareUrl)
|
||||
alert(locale.COMMON.URL_COPIED)
|
||||
}
|
||||
|
||||
return <>
|
||||
<div
|
||||
className='py-2 text-gray-500 text-center space-x-2 flex my-1 dark:text-gray-200 overflow-visible'>
|
||||
<div className='hidden md:block text-gray-800 dark:text-gray-300 mr-2 my-2 whitespace-nowrap'>{locale.COMMON.SHARE}:</div>
|
||||
<div className='text-3xl cursor-pointer'>
|
||||
<a className='text-blue-700' href={`https://www.facebook.com/sharer.php?u=${shareUrl}`} >
|
||||
<FontAwesomeIcon icon={faFacebookSquare}/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='text-3xl cursor-pointer'>
|
||||
<a className='text-blue-400' target='_blank' rel='noreferrer' href={`https://twitter.com/intent/tweet?title=${post.title}&url${shareUrl}`} >
|
||||
<FontAwesomeIcon icon={faTwitterSquare}/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='text-3xl cursor-pointer'>
|
||||
<a className='text-blue-500' href={`https://telegram.me/share/url?url=${shareUrl}&text=${post.title}`} >
|
||||
<FontAwesomeIcon icon={faTelegram}/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='cursor-pointer text-2xl'>
|
||||
<a className='text-green-600' ref={btnRef} onMouseEnter={openPopover} onMouseLeave={closePopover}>
|
||||
<FontAwesomeIcon icon={faWeixin}/>
|
||||
<div ref={popoverRef} className={(qrCodeShow ? 'animate__animated animate__fadeIn ' : 'hidden') + ' text-center py-2'}>
|
||||
<div className='p-2 bg-white border-0 duration-200 transform block z-50 font-normal shadow-xl mr-10'>
|
||||
<QRCode value={shareUrl} fgColor='#000000' />
|
||||
</div>
|
||||
<span className='bg-white text-black font-semibold p-1 mb-0 rounded-t-lg text-sm mx-auto mr-10'>
|
||||
{locale.COMMON.SCAN_QR_CODE}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className='cursor-pointer text-2xl'>
|
||||
<a className='text-red-600' target='_blank' rel='noreferrer'href={`https://service.weibo.com/share/share.php?url=${shareUrl}&title=${post.title}`} >
|
||||
<FontAwesomeIcon icon={faWeibo}/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='cursor-pointer text-2xl'>
|
||||
<a className='text-blue-400' target='_blank' rel='noreferrer'href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${post.title}&desc=${post.summary}`} >
|
||||
<FontAwesomeIcon icon={faQq}/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='cursor-pointer text-2xl'>
|
||||
<a className='text-yellow-600' onClick={copyUrl} >
|
||||
<FontAwesomeIcon icon={faLink}/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
export default ShareBar
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faShareAltSquare } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 悬浮在屏幕右下角,分享按钮
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ShareButton = ({ post }) => {
|
||||
const [popoverShow, setPopoverShow] = React.useState(false)
|
||||
const btnRef = React.createRef()
|
||||
|
||||
const openPopover = () => {
|
||||
setPopoverShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setPopoverShow(false)
|
||||
}
|
||||
return (
|
||||
<div className='my-2'
|
||||
onMouseEnter={() => { openPopover() }}
|
||||
onMouseLeave={() => { closePopover() }}>
|
||||
<div className={(popoverShow ? 'opacity-100' : 'opacity-0') + ' duration-200 ease-in-out font-normal'}>
|
||||
<ShareBar post={post}/>
|
||||
</div>
|
||||
<div ref={btnRef}
|
||||
className='z-20 border dark:border-gray-500 dark:bg-gray-600 bg-white cursor-pointer text-md hover:shadow-2xl shadow-lg'>
|
||||
<FontAwesomeIcon icon={faShareAltSquare} className='transform duration-200 hover:scale-150 dark:text-gray-200 p-4' title='share' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareButton
|
||||
@@ -1,60 +0,0 @@
|
||||
import InfoCard from '@/components/InfoCard'
|
||||
import MenuButtonGroup from '@/components/MenuButtonGroup'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import Toc from '@/components/Toc'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React from 'react'
|
||||
import Analytics from './Analytics'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import BLOG from '@/blog.config'
|
||||
import Logo from './Logo'
|
||||
import Card from './Card'
|
||||
|
||||
/**
|
||||
* 侧边平铺
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @param post
|
||||
* @param posts
|
||||
* @param categories
|
||||
* @param currentCategory
|
||||
* @param currentSearch
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideAreaLeft = ({ title, tags, currentTag, post, postCount, categories, currentCategory, currentSearch, targetRef }) => {
|
||||
const { locale } = useGlobal()
|
||||
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-gray-800 hover:shadow-xl duration-200'>
|
||||
<Logo/>
|
||||
<div className='pt-2 px-2 font-sans'>
|
||||
<MenuButtonGroup allowCollapse={true} postCount={postCount} />
|
||||
</div>
|
||||
{BLOG.menu.showSearch && <div className='px-2 pt-2 font-sans'>
|
||||
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
|
||||
</div>}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<Card className='sticky top-4'>
|
||||
<Tabs>
|
||||
{showToc && (
|
||||
<div key={locale.COMMON.TABLE_OF_CONTENTS} className='dark:text-gray-400 text-gray-600 bg-white dark:bg-gray-800 duration-200'>
|
||||
<Toc toc={post.toc}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div key={locale.NAV.ABOUT} className='mb-5 bg-white dark:bg-gray-800 duration-200 py-6'>
|
||||
<InfoCard />
|
||||
<Analytics postCount={postCount}/>
|
||||
</div>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
</aside>
|
||||
}
|
||||
export default SideAreaLeft
|
||||
@@ -1,98 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faAngleDoubleRight, faAngleRight, faTag, faThList } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import Card from './Card'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import TagGroups from './TagGroups'
|
||||
|
||||
/**
|
||||
* 侧边平铺
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @param post
|
||||
* @param posts
|
||||
* @param categories
|
||||
* @param currentCategory
|
||||
* @param currentSearch
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideAreaRight = ({
|
||||
title,
|
||||
tags,
|
||||
currentTag,
|
||||
post,
|
||||
slot,
|
||||
categories,
|
||||
currentCategory,
|
||||
currentSearch,
|
||||
targetRef
|
||||
}) => {
|
||||
const { locale } = useGlobal()
|
||||
const { widget } = BLOG
|
||||
if (!widget?.showCategoryList && !widget.showTagList && !widget.showLatestPost) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (<aside id='right' className='hidden 2xl:block flex-col w-60 ml-4'>
|
||||
|
||||
<Card className='mb-2'>
|
||||
{/* 展示广告 */}
|
||||
<ins
|
||||
className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-adtest="on"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="8807314373"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
></ins>
|
||||
</Card>
|
||||
|
||||
<div className="sticky top-4">
|
||||
|
||||
{/* 分类 */}
|
||||
{widget?.showCategoryList && categories && (
|
||||
<Card>
|
||||
<div className='text-sm px-2 flex flex-nowrap justify-between font-light'>
|
||||
<div className='pb-1 text-gray-600 dark:text-gray-300'><FontAwesomeIcon icon={faThList} className='mr-2' />{locale.COMMON.CATEGORY}</div>
|
||||
<Link href='/category' passHref>
|
||||
<a className='text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleRight} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{slot}
|
||||
|
||||
{widget?.showTagList && tags && (
|
||||
<Card>
|
||||
<div className="text-sm pb-1 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="px-2 pt-2">
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
export default SideAreaRight
|
||||
@@ -1,81 +0,0 @@
|
||||
import CategoryGroup from '@/components/CategoryGroup'
|
||||
import InfoCard from '@/components/InfoCard'
|
||||
import TagGroups from '@/components/TagGroups'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faAngleDoubleRight, faTag, faThList } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 侧边栏
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @param post
|
||||
* @param posts
|
||||
* @param categories
|
||||
* @param currentCategory
|
||||
* @param currentSearch
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideBar = ({ title, tags, currentTag, post, slot, categories, currentCategory, currentSearch }) => {
|
||||
const { locale } = useGlobal()
|
||||
return <aside id='sidebar' className='bg-white dark:bg-gray-900 w-80 z-10 dark:border-gray-500 border-gray-200 scroll-hidden h-full'>
|
||||
|
||||
<div className={(!post ? 'sticky top-0' : '') + ' bg-white dark:bg-gray-900 pb-4'}>
|
||||
|
||||
<section className='py-5'>
|
||||
<InfoCard />
|
||||
</section>
|
||||
|
||||
{/* 分类 */}
|
||||
{categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm px-5 flex flex-nowrap justify-between font-light'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faThList} className='mr-2' />{locale.COMMON.CATEGORY}</div>
|
||||
<Link href='/category' passHref>
|
||||
<a className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 标签云 */}
|
||||
{tags && (
|
||||
<section className='mt-4'>
|
||||
<div className='text-sm py-2 px-5 flex flex-nowrap justify-between font-light dark:text-gray-200'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faTag} className='mr-2'/>{locale.COMMON.TAGS}</div>
|
||||
<Link href='/tag' passHref>
|
||||
<a className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='px-5 py-2'>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{slot}
|
||||
|
||||
</div>
|
||||
|
||||
<section className='bg-white dark:bg-gray-900'>
|
||||
{/* 信息流广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-adtest="on"
|
||||
data-ad-format="fluid"
|
||||
data-ad-layout-key="-5j+cz+30-f7+bf"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="1510444138"></ins>
|
||||
</section>
|
||||
|
||||
</aside>
|
||||
}
|
||||
export default SideBar
|
||||
@@ -1,59 +0,0 @@
|
||||
import SideBar from '@/components/SideBar'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useImperativeHandle } from 'react'
|
||||
|
||||
/**
|
||||
* 侧边栏抽屉面板,可以从侧面拉出
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SideBarDrawer = ({ post, currentTag, cRef, tags, slot, categories, currentCategory }) => {
|
||||
// 暴露给父组件 通过cRef.current.handleMenuClick 调用
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
handleSwitchSideDrawerVisible: () => switchSideDrawerVisible(true)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const sideBarWrapperElement = document.getElementById('sidebar-wrapper')
|
||||
sideBarWrapperElement?.classList?.remove('hidden')
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
const sideBarDrawerRouteListener = url => {
|
||||
switchSideDrawerVisible(false)
|
||||
}
|
||||
router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
// 点击按钮更改侧边抽屉状态
|
||||
const switchSideDrawerVisible = (showStatus) => {
|
||||
if (window) {
|
||||
const sideBarDrawer = window.document.getElementById('sidebar-drawer')
|
||||
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
|
||||
|
||||
if (showStatus) {
|
||||
sideBarDrawer.classList.replace('-ml-80', 'ml-0')
|
||||
sideBarDrawerBackground.classList.replace('hidden', 'block')
|
||||
} else {
|
||||
sideBarDrawer.classList.replace('ml-0', '-ml-80')
|
||||
sideBarDrawerBackground.classList.replace('block', 'hidden')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <div id='sidebar-wrapper' className='hidden'>
|
||||
<div id='sidebar-drawer' className='-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-50'>
|
||||
<SideBar tags={tags} post={post} slot={slot} categories={categories} currentCategory={currentCategory} />
|
||||
</div>
|
||||
{/* 背景蒙版 */}
|
||||
<div id='sidebar-drawer-background' onClick={() => { switchSideDrawerVisible(false) }} className='hidden animate__animated animate__fadeIn fixed top-0 duration-300 left-0 z-30 w-full h-full glassmorphism'/>
|
||||
|
||||
</div>
|
||||
}
|
||||
export default SideBarDrawer
|
||||
@@ -1,36 +0,0 @@
|
||||
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-52 justify-center flex-wrap flex'>
|
||||
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 '>
|
||||
{BLOG.socialLink.github && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.socialLink.github} >
|
||||
<FontAwesomeIcon icon={faGithub} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.socialLink.twitter && <a target='_blank' rel='noreferrer' title={'twitter'} href={BLOG.socialLink.twitter} >
|
||||
<FontAwesomeIcon icon={faTwitter} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.socialLink.telegram && <a target='_blank' rel='noreferrer' href={BLOG.socialLink.telegram} title={'telegram'} >
|
||||
<FontAwesomeIcon icon={faTelegram} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.socialLink.weibo && <a target='_blank' rel='noreferrer' title={'weibo'} href={BLOG.socialLink.weibo} >
|
||||
<FontAwesomeIcon icon={faWeibo} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.email && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.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
|
||||
@@ -1,52 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
/**
|
||||
* 标签组导航条,默认隐藏仅在移动端显示
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const StickyBar = ({ children }) => {
|
||||
if (!children) return <></>
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
if (BLOG.topNavType === 'normal') {
|
||||
return
|
||||
}
|
||||
const scrollS = window.scrollY
|
||||
if (scrollS >= windowTop && scrollS > 10) {
|
||||
const stickyBar = document.querySelector('#sticky-bar')
|
||||
stickyBar && stickyBar.classList.replace('top-14', 'top-0')
|
||||
windowTop = scrollS
|
||||
} else {
|
||||
const stickyBar = document.querySelector('#sticky-bar')
|
||||
stickyBar && stickyBar.classList.replace('top-0', 'top-14')
|
||||
windowTop = scrollS
|
||||
}
|
||||
}, 200))
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
scrollTrigger()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id='sticky-bar' className='sticky flex-grow justify-center top-0 duration-500 z-10 pb-16'>
|
||||
<div className='glassmorphism dark:border-gray-600 px-5 absolute shadow-md border w-full hidden-scroll'>
|
||||
<div id='tag-container' className="md:pl-3 overflow-x-auto">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StickyBar
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* Tabs切换标签
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const Tabs = ({ children }) => {
|
||||
if (!children) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
let count = children.length
|
||||
children.forEach(e => {
|
||||
if (!e) {
|
||||
count--
|
||||
}
|
||||
})
|
||||
|
||||
if (count === 1) {
|
||||
return <section className='duration-200'>
|
||||
{children}
|
||||
</section>
|
||||
}
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(0)
|
||||
function tabClickHandle (i) {
|
||||
setCurrentTab(i)
|
||||
}
|
||||
|
||||
return (
|
||||
< >
|
||||
{<div className='hidden lg:block mb-5 bg-white dark:bg-gray-800 duration-200'>
|
||||
<ul className='flex justify-center space-x-5 pb-4 dark:text-gray-400 text-gray-600'>
|
||||
{children.map((item, index) => {
|
||||
return <li key={index} className={(currentTab === index ? 'font-black border-b-2 border-red-400 text-red-400 animate__animated animate__jello ' : 'font-extralight cursor-pointer') + ' text-sm font-sans '} onClick={() => { tabClickHandle(index) }}>
|
||||
{item?.key}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
{children.map((item, index) => {
|
||||
return <section key={index} className={`${currentTab === index ? 'block animate__animated animate__fadeIn animate__faster' : 'hidden'}`}>
|
||||
{item}
|
||||
</section>
|
||||
})}
|
||||
</div>}
|
||||
|
||||
</>)
|
||||
}
|
||||
|
||||
export default Tabs
|
||||
@@ -1,24 +0,0 @@
|
||||
import TagItemMini from '@/components/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 w-66 space-y-2'>
|
||||
{
|
||||
tags.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagGroups
|
||||
@@ -1,26 +0,0 @@
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const TagItem = ({ tag, selected }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (!tag) {
|
||||
<>{locale.COMMON.NOTAG}</>
|
||||
}
|
||||
return (
|
||||
<Link href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<li
|
||||
className={`notion-${tag.color}_background dark:bg-gray-700 list-none cursor-pointer rounded-md
|
||||
duration-200 mr-1 my-1 px-2 py-1 text-sm whitespace-nowrap
|
||||
hover:bg-gray-200 dark:hover:bg-gray-800 `}>
|
||||
<div className='text-gray-600 dark:text-gray-300 dark:hover:text-white'>
|
||||
{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {`${tag.name} `} {tag.count ? `(${tag.count})` : ''}
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagItem
|
||||
@@ -1,17 +0,0 @@
|
||||
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-gray-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-gray-900'
|
||||
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}` }>
|
||||
<div className='font-light dark:text-gray-400'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
export default TagItemMini
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import TagItem from '@/components/TagItem'
|
||||
/**
|
||||
* 横向的标签列表
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TagList = ({ tags, currentTag }) => {
|
||||
if (!tags) {
|
||||
return <></>
|
||||
}
|
||||
return <ul className='flex py-1 space-x-3'>
|
||||
<li className='w-20 py-2 dark:text-gray-200 whitespace-nowrap'>标签:</li>
|
||||
{tags.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItem key={tag.name} tag={tag} selected={selected}/>
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
|
||||
export default TagList
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { uuidToId } from 'notion-utils'
|
||||
import Progress from './Progress'
|
||||
// import { cs } from 'react-notion-x'
|
||||
|
||||
/**
|
||||
* 目录导航组件
|
||||
* @param toc
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Toc = ({ toc }) => {
|
||||
// 无目录就直接返回空
|
||||
if (!toc || toc.length < 1) {
|
||||
return <></>
|
||||
}
|
||||
// 监听滚动事件
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', actionSectionScrollSpy)
|
||||
actionSectionScrollSpy()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', actionSectionScrollSpy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 同步选中目录事件
|
||||
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)
|
||||
}, throttleMs))
|
||||
|
||||
return <div className='px-3'>
|
||||
<div className='w-full pb-1'>
|
||||
<Progress/>
|
||||
</div>
|
||||
<nav className='font-sans overflow-y-auto scroll-hidden'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
return (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={`notion-table-of-contents-item duration-300 transform font-light
|
||||
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginLeft: tocItem.indentLevel * 16
|
||||
}}
|
||||
className={`${activeSection === id && ' font-bold text-red-400 underline'}`}
|
||||
>
|
||||
{tocItem.text}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Toc
|
||||
@@ -1,47 +0,0 @@
|
||||
import Toc from '@/components/Toc'
|
||||
import React, { useImperativeHandle, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 目录抽屉栏
|
||||
* @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)
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
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 max-h-96 ' +
|
||||
' w-60 duration-200 fixed right-4 top-16 rounded overflow-y-auto py-2 bg-white dark:bg-gray-600'}>
|
||||
{post && <>
|
||||
<div className='font-bold pb-2 text-center text-black dark:text-white '>
|
||||
{locale.COMMON.TABLE_OF_CONTENTS}
|
||||
</div>
|
||||
<div className='dark:text-gray-400 text-gray-600 dark:bg-gray-800'>
|
||||
<Toc 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
|
||||
@@ -1,24 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faListOl } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 点击召唤目录抽屉
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param props 父组件传入props
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TocDrawerButton = (props) => {
|
||||
if (!BLOG.widget?.showToc) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return (<div onClick={props.onClick} className='py-2 px-3 cursor-pointer dark:text-gray-200 text-center transform hover:scale-150 duration-200 flex justify-center items-center' title={locale.POST.TOP} >
|
||||
<FontAwesomeIcon icon={faListOl}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TocDrawerButton
|
||||
@@ -1,123 +0,0 @@
|
||||
import BLOG from '@/blog.config'
|
||||
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'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
/**
|
||||
* 顶部导航
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, autoHide = true, 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-16')
|
||||
windowTop = scrollS
|
||||
} else {
|
||||
const nav = document.querySelector('#sticky-nav')
|
||||
nav && nav.classList.replace('-top-16', 'top-0')
|
||||
windowTop = scrollS
|
||||
}
|
||||
}, 200), [])
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
if (BLOG.topNavType === 'autoCollapse') {
|
||||
scrollTrigger()
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
return () => {
|
||||
BLOG.autoCollapsedNavBar && window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
const searchDrawerSlot = <>
|
||||
{ categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm 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={`${BLOG.topNavType !== '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
|
||||
@@ -1,58 +0,0 @@
|
||||
import { faClock, faFileWord } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount () {
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <div id='wordCountWrapper' className='hidden'>
|
||||
<FontAwesomeIcon icon={faFileWord} className='mr-1'/> 本文字数 <strong id='wordCount'>0</strong> | <FontAwesomeIcon className='mr-1' icon={faClock}/> 阅读时长 ≈ <strong id='readTime'>0</strong> 分钟
|
||||
</div>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords () {
|
||||
if (window) {
|
||||
const articleElement = document.getElementById('notion-article')
|
||||
if (articleElement) {
|
||||
const articleText = deleteHtmlTag(articleElement.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) {
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords (str) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user