This commit is contained in:
tangly1024
2021-09-27 09:33:21 +08:00
parent 22ca7f6d63
commit dfc0f645d4
76 changed files with 3650 additions and 2 deletions

14
components/Ackee.js Normal file
View File

@@ -0,0 +1,14 @@
import { useRouter } from 'next/router'
import useAckee from 'use-ackee'
const Ackee = ({ ackeeServerUrl, ackeeDomainId }) => {
const router = useRouter()
useAckee(
router.asPath,
{ server: ackeeServerUrl, domainId: ackeeDomainId },
{ detailed: false, ignoreLocalhost: true }
)
return null
}
export default Ackee

25
components/BlogPost.js Normal file
View File

@@ -0,0 +1,25 @@
import BLOG from '@/blog.config'
const BlogPost = ({ post }) => {
return (
<article key={post.id}
className='md:mx-2 w-full md:max-w-md duration-200 transform hover:scale-105 hover:shadow-2xl bg-white dark:bg-gray-800 dark:hover:bg-gray-600 overflow-hidden'>
{/* 封面图 */}
{post.page_cover && post.page_cover.length > 1 && (
<a href={`${BLOG.path}/article/${post.slug}`} className='md:flex-shrink-0 md:w-52 md:h-52 rounded-lg'>
<img className='w-full max-h-60 object-cover p-3 cursor-pointer' src={post.page_cover} alt={post.title} />
</a>
)}
<main className='px-8 py-2'>
<a href={`${BLOG.path}/article/${post.slug}`}
className='block my-3 text-2xl leading-tight font-semibold text-black dark:text-gray-200 hover:underline'>
{post.title}
</a>
<p className='mt-2 text-gray-500 dark:text-gray-400 text-sm'>{post.summary}</p>
</main>
</article>
)
}
export default BlogPost

View File

@@ -0,0 +1,24 @@
import BLOG from '@/blog.config'
const BlogPostMini = ({ post }) => {
return (
<a key={post.id} href={`${BLOG.path}/article/${post.slug}`}
className='md:flex w-full border my-2 duration-200 transform hover:scale-105 hover:shadow-2xl bg-white dark:bg-gray-800 dark:hover:bg-gray-600'>
{/* 封面图 */}
{post.page_cover && post.page_cover.length > 1 && (
<img className='md:w-40 w-full max-h-32 object-cover cursor-pointer' src={post.page_cover} alt={post.title} />
)}
<main className='px-2 py-1'>
<a href={`${BLOG.path}/article/${post.slug}`}
className='block my-3 leading-tight font-semibold text-black dark:text-gray-200 hover:underline'>
{post.title}
</a>
<p className='mt-2 text-gray-500 dark:text-gray-400 text-xs overflow-x-hidden'>{post.summary}</p>
<p className='mt-2 text-gray-500 dark:text-gray-400 text-xs overflow-x-hidden'>{BLOG.link}/article/{post.slug}</p>
</main>
</a>
)
}
export default BlogPostMini

67
components/Comment.js Normal file
View File

@@ -0,0 +1,67 @@
import BLOG from '@/blog.config'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
const GitalkComponent = dynamic(
() => {
return import('gitalk/dist/gitalk-component')
},
{ ssr: false }
)
const UtterancesComponent = dynamic(
() => {
return import('@/components/Utterances')
},
{ ssr: false }
)
const CusdisComponent = dynamic(
() => {
return import('react-cusdis').then(m => m.ReactCusdis)
},
{ ssr: false }
)
const Comment = ({ frontMatter }) => {
const router = useRouter()
return <div className='comment'>
<div className='font-bold text-gray-800 pt-2 pb-4 dark:text-gray-300'>留下评论</div>
{/* 评论插件 */}
{BLOG.comment.provider === 'gitalk' && (
<GitalkComponent
options={{
id: frontMatter.id,
title: frontMatter.title,
clientID: BLOG.comment.gitalkConfig.clientID,
clientSecret: BLOG.comment.gitalkConfig.clientSecret,
repo: BLOG.comment.gitalkConfig.repo,
owner: BLOG.comment.gitalkConfig.owner,
admin: BLOG.comment.gitalkConfig.admin,
distractionFreeMode: BLOG.comment.gitalkConfig.distractionFreeMode
}}
/>
)}
{BLOG.comment.provider === 'utterances' && (
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
)}
{BLOG.comment.provider === 'cusdis' && (
<>
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
<CusdisComponent
attrs={{
host: BLOG.comment.cusdisConfig.host,
appId: BLOG.comment.cusdisConfig.appId,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.link + router.asPath,
theme: BLOG.appearance
}}
lang={BLOG.lang.toLowerCase()}
/>
</>
)}</div>
}
export default Comment

45
components/CommonHead.js Normal file
View File

@@ -0,0 +1,45 @@
import BLOG from '@/blog.config'
import Head from 'next/head'
const CommonHead = ({ meta }) => {
const url = BLOG.path.length ? `${BLOG.link}/${BLOG.path}` : BLOG.link
return <Head>
<title>{meta.title}</title>
<meta content={BLOG.darkBackground} name='theme-color' />
<meta name='robots' content='follow, index' />
<meta charSet='UTF-8' />
{BLOG.seo.googleSiteVerification && (
<meta
name='google-site-verification'
content={BLOG.seo.googleSiteVerification}
/>
)}
{BLOG.seo.keywords && (
<meta name='keywords' content={BLOG.seo.keywords.join(', ')} />
)}
<meta name='description' content={meta.description} />
<meta property='og:locale' content={BLOG.lang} />
<meta property='og:title' content={meta.title} />
<meta property='og:description' content={meta.description} />
<meta
property='og:url'
content={meta.slug ? `${url}/${meta.slug}` : url}
/>
<meta property='og:type' content={meta.type} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={meta.description} />
<meta name='twitter:title' content={meta.title} />
{meta.type === 'article' && (
<>
<meta
property='article:published_time'
content={meta.date || meta.createdTime}
/>
<meta property='article:author' content={BLOG.author} />
</>
)}
</Head>
}
export default CommonHead

View File

@@ -0,0 +1,22 @@
import React from 'react'
import Link from 'next/link'
/**
* 悬浮在屏幕右下角,联系我的按钮
* @returns {JSX.Element}
* @constructor
*/
const ContactButton = () => {
return (
<Link href='/article/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'>
<span className='dark:text-gray-200 fa fa-info' title='about' />
</span>
</a>
</Link>
)
}
export default ContactButton

16
components/Container.js Normal file
View File

@@ -0,0 +1,16 @@
import PropTypes from 'prop-types'
const Container = ({ children, layout, fullWidth, ...customMeta }) => {
return (
<div>
{/* 公共头 */}
{children}
</div>
)
}
Container.propTypes = {
children: PropTypes.node
}
export default Container

33
components/Cusdis.js Normal file
View File

@@ -0,0 +1,33 @@
import BLOG from '@/blog.config'
import { useEffect } from 'react'
const Cusdis = ({ id, url, title }) => {
useEffect(() => {
const script = document.createElement('script')
const anchor = document.getElementById('comments')
script.setAttribute(
'src',
BLOG.comment.cusdisConfig.scriptSrc ||
'https://cusdis.com/js/cusdis.es.js'
)
script.setAttribute('async', true)
script.setAttribute('defer', true)
anchor.appendChild(script)
return () => {
anchor.innerHTML = ''
}
})
return (
<div id="comments">
<div
id="cusdis_thread"
data-host={BLOG.comment.cusdisConfig.host || 'https://cusdis.com'}
data-app-id={BLOG.comment.cusdisConfig.appId}
data-page-id={id}
data-page-url={url}
data-page-title={title}
></div>
</div>
)
}
export default Cusdis

View File

@@ -0,0 +1,19 @@
import { useTheme } from '@/lib/theme'
import localStorage from 'localStorage'
const DarkModeButton = () => {
const { theme, changeTheme } = useTheme()
const handleChangeDarkMode = () => {
const newTheme = (theme === 'light' ? 'dark' : 'light')
changeTheme(newTheme)
localStorage.setItem('theme', newTheme)
}
return <div className=''>
<div onClick={handleChangeDarkMode}
className='border w-10 h-10 justify-center align-middle font-bold text-lg rounded flex p-2.5 cursor-pointer text-gray-600 hover:scale-125 transform duration-200
dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black'>
<span className={'fa px-1 ' + (theme === 'dark' ? ' fa-sun-o' : ' fa-moon-o')} />
</div>
</div>
}
export default DarkModeButton

23
components/Footer.js Normal file
View File

@@ -0,0 +1,23 @@
import BLOG from '@/blog.config'
import React from 'react'
import SocialButton from '@/components/SocialButton'
const Footer = ({ fullWidth = true }) => {
const d = new Date()
const y = d.getFullYear()
const from = +BLOG.since
return (
<div
className='py-4 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 bottom-0'
>
<SocialButton/>
<div className='text-sm'>
<span className='fa fa-shield leading-6'><a href='https://beian.miit.gov.cn/' className='ml-1'>闽ICP备20010331号</a></span>
<br/>
<span className='fa fa-copyright leading-6'> {from === y || !from ? y : `${from} - ${y}`} {BLOG.author} </span>
</div>
</div>
)
}
export default Footer

18
components/Gtag.js Normal file
View File

@@ -0,0 +1,18 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import * as gtag from '@/lib/gtag'
const Gtag = () => {
const router = useRouter()
useEffect(() => {
const handleRouteChange = url => {
gtag.pageview(url)
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return null
}
export default Gtag

94
components/Header.js Normal file
View File

@@ -0,0 +1,94 @@
import { useEffect, useRef } from 'react'
import Link from 'next/link'
import BLOG from '@/blog.config'
import Image from 'next/image'
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 }) => {
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 (
<>
<div className='observer-element h-0.5' ref={sentinelRef}/>
<div
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 py-8 bg-opacity-60 ${
!fullWidth ? 'max-w-5xl px-4' : 'px-4 md:px-24'
}`}
id='sticky-nav'
ref={navRef}
>
<div className='flex items-center'>
<Link href='/'>
<a>
<div className='h-6'>
<Image
alt={BLOG.author}
width={24}
height={24}
src='/favicon.svg'
className='rounded-full'
/>
</div>
</a>
</Link>
{navBarTitle
? (
<p className='ml-2 font-medium text-day dark:text-night header-name'>
{navBarTitle}
</p>
)
: (
<p className='ml-2 font-medium text-day dark:text-night header-name'>
{BLOG.title} {' '}
{BLOG.title},{' '}
<span className='font-normal'>{BLOG.description}</span>
</p>
)}
</div>
<NavBar />
</div>
</>
)
}
export default Header

70
components/LeftAside.js Normal file
View File

@@ -0,0 +1,70 @@
import Tags from '@/components/Tags'
import { useLocale } from '@/lib/locale'
import Link from 'next/link'
import BLOG from '@/blog.config'
import { useState } from 'react'
import Router, { useRouter } from 'next/router'
import DarkModeButton from '@/components/DarkModeButton'
import SocialButton from '@/components/SocialButton'
import Footer from '@/components/Footer'
const LeftAside = ({ tags, currentTag }) => {
const locale = useLocale()
const router = useRouter()
const [searchValue, setSearchValue] = useState('')
const handleKeyUp = (e) => {
if (e.keyCode === 13) {
Router.push({ pathname: '/', query: { s: searchValue } })
}
}
return <aside
style={{ width: '330px' }}
className='px-10 hidden xl:block py-5 bg-gray-50 dark:bg-gray-800 duration-200 border-r dark:border-black'
>
<div className='sticky top-16'>
<div className='my-5 flex'>
<Link href='/'>
<a
className='hover:shadow-xl dark:border-gray-600 border-black border-2 bg-white dark:bg-gray-800 dark:text-gray-300 font-semibold hover:bg-gray-800 hover:text-white p-2 duration-200'>{BLOG.title}</a>
</Link>
</div>
<div className='text-gray-500 dark:text-gray-300'>
<i className='fa fa-map-marker mr-1' />
Fuzhou, China
</div>
<hr className='my-5'/>
{/* 搜索框 */}
<div className='flex justify-center items-center py-5 '>
<i className='fa fa-search absolute right-8 text-gray-400' />
<input
type='text'
placeholder={
currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`
}
className='hover:shadow-xl duration-200 px-5 bg-gray-100 rounded w-full py-2 border-black dark:border-gray-600 bg-white text-black dark:bg-gray-700 dark:text-white'
onKeyUp={handleKeyUp}
onChange={e => setSearchValue(e.target.value)}
defaultValue={router.query.s ?? ''}
/>
</div>
<hr className='my-5'/>
<div>
<span className='dark:text-gray-200'>标签</span>
<Tags tags={tags} currentTag={currentTag} />
</div>
<div className='bottom-1 fixed'>
<div className='justify-center flex '><DarkModeButton /></div>
<Footer/>
</div>
</div>
</aside>
}
export default LeftAside

44
components/Pagination.js Normal file
View File

@@ -0,0 +1,44 @@
import BLOG from '@/blog.config'
import { useLocale } from '@/lib/locale'
import Link from 'next/link'
import { useRouter } from 'next/router'
const Pagination = ({ page, showNext }) => {
const locale = useLocale()
const router = useRouter()
const currentPage = +page
return (
<div className=' my-10 flex justify-between font-medium text-black dark:text-gray-100 mx-5'>
<Link
href={
{
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`),
query: router.query.s ? { s: router.query.s } : {}
}
}
>
<button
rel='prev'
className={`${currentPage === 1 ? 'invisible' : 'block'} 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 } : {}
}
}>
<button
rel='next'
className={`${+showNext ? 'block' : 'invisible'} duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
>
{locale.PAGINATION.NEXT}
</button>
</Link>
</div>
)
}
export default Pagination

35
components/Progress.js Normal file
View File

@@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react'
import throttle from 'lodash.throttle'
/**
* 跳转到网页顶部当屏幕下滑500像素后会出现该控件
* @returns {JSX.Element}
* @constructor
*/
const Progress = ({ targetRef }) => {
const [percent, changePercent] = useState(0)
useEffect(() => {
const scrollListener = throttle(() => {
if (targetRef.current) {
const fullHeight = targetRef.current.clientHeight
const per = parseFloat(((window.scrollY / (fullHeight) * 100)).toFixed(0))
changePercent(per)
}
// console.log('滚动信息', window.scrollY, fullHeight, per)
}, 1)
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [percent])
return (
<>
{/* 顶部进度条 */}
<div className='h-1.5 fixed top-0 w-full shadow-2xl z-40'>
<div className='h-1 bg-blue-500 fixed top-0 w-1 duration-200' style={{ width: `${percent}%` }}/>
{/* <div className='debug'>{percent}</div> */}
</div>
</>
)
}
export default Progress

View File

@@ -0,0 +1,63 @@
import React from 'react'
import { createPopper } from '@popperjs/core'
/**
* 赞赏模块
* @returns {JSX.Element}
* @constructor
*/
const RewardButton = () => {
const [popoverShow, setPopoverShow] = React.useState(false)
const btnRef = React.createRef()
const popoverRef = React.createRef()
const openPopover = () => {
createPopper(btnRef.current, popoverRef.current, {
placement: 'top'
})
setPopoverShow(true)
}
const closePopover = () => {
setPopoverShow(false)
}
return (
<div
onMouseEnter={() => {
openPopover()
}}
onMouseLeave={() => {
closePopover()
}}>
<div className='animate__jello animate__animated animate__faster'>
<div
ref={btnRef}
className='bg-blue-500 text-white hover:bg-white hover:text-black hover:shadow-2xl border duration-200 transform hover:scale-110 px-3 py-2 rounded cursor-pointer'>
<div>
<span className='fa fa-qrcode mr-2' />
<span>打赏</span>
</div>
</div>
</div>
<div
className={
(popoverShow ? 'animate__animated animate__fadeIn ' : 'hidden ') +
' animate__faster border-0 transform block z-50 font-normal'
}
ref={popoverRef}
>
<div>
<div
className='border animate__animated animate__fadeIn hover:shadow-2xl duration-200 my-5 px-5 py-6 w-96 grid justify-center bg-white dark:bg-black dark:text-gray-200'>
<span>
<img className='md:w-72 m-auto' src='/reward_code.jpg' />
</span>
<br />
<span className='text-center text-gray-500'>微信赞赏码或支付宝tlyong@126.com赞助</span>
</div>
</div>
</div>
</div>
)
}
export default RewardButton

14
components/RightAside.js Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react'
import Toc from '@/components/Toc'
const RightAside = ({ toc }) => {
// 无目录就直接返回空
if (toc.length < 1) return <></>
return <aside className='bg-gray-800 px-5 hidden lg:block py-5 hover:shadow-2xl duration-200'>
<div className='sticky top-8 w-60 overflow-x-auto'>
<Toc toc={toc}/>
</div>
</aside>
}
export default RightAside

13
components/RightWidget.js Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import TopJumper from '@/components/TopJumper'
import ShareButton from '@/components/ShareButton'
const RightWidget = ({ post }) => {
return <div className='fixed right-0 lg:mr-72 bottom-10 flex justify-center'>
<div className='flex-wrap'>
<ShareButton post={post}/>
<TopJumper/>
</div>
</div>
}
export default RightWidget

84
components/ShareBar.js Normal file
View File

@@ -0,0 +1,84 @@
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'
const ShareBar = ({ post }) => {
const router = useRouter()
const shareUrl = BLOG.link + router.asPath
// 二维码悬浮
const [qrCodeShow, setQrCodeShow] = React.useState(false)
const btnRef = React.createRef()
const popoverRef = React.createRef()
const openPopover = () => {
createPopper(btnRef.current, popoverRef.current, {
placement: 'left'
})
setQrCodeShow(true)
}
const closePopover = () => {
setQrCodeShow(false)
}
const copyUrl = () => {
copy(shareUrl)
alert('当前链接已复制到剪贴板')
}
return <>
<div className='text-gray-500 flex-col text-center space-y-2 w-12 border my-1 bg-white dark:bg-gray-800 dark:text-white overflow-hidden'>
<div>
分享
</div>
<div>
<a className='fa fa-facebook-square cursor-pointer text-3xl'
href={`https://www.facebook.com/sharer.php?u=${shareUrl}`} />
</div>
<div>
<a className='fa fa-twitter-square text-3xl' target='_blank' rel='noreferrer'
href={`https://twitter.com/intent/tweet?title=${post.title}&url${shareUrl}`} />
</div>
<div>
<a className='fa fa-telegram text-3xl' href={`https://telegram.me/share/url?url=${shareUrl}&text=${post.title}`} />
</div>
<div>
<a className='fa fa-wechat cursor-pointer text-3xl' ref={btnRef}
onMouseEnter={() => { openPopover() }}
onMouseLeave={() => { closePopover() }}>
<div ref={popoverRef}
className={(qrCodeShow ? 'animate__animated animate__fadeIn ' : 'hidden') + ' text-center py-2 bg-white'}>
<div className='p-2 bg-white border-0 duration-200 transform block z-50 font-normal shadow-xl'>
<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'>
扫一扫分享
</span>
</div>
</a>
</div>
<div>
<a className='fa fa-weibo text-3xl' target='_blank' rel='noreferrer'
href={`https://service.weibo.com/share/share.php?url=${shareUrl}&title=${post.title}`} />
</div>
<div>
<a className='fa fa-qq text-3xl' target='_blank' rel='noreferrer'
href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${post.title}&desc=${post.summary}`} />
</div>
<div>
<a className='fa fa-star text-3xl' target='_blank' rel='noreferrer'
href={`https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${shareUrl}&sharesource=qzone&title=${post.title}&summary=${post.summary}`} />
</div>
<div>
<a className='fa fa-link cursor-pointer text-3xl' onClick={() => { copyUrl() }} />
</div>
</div>
</>
}
export default ShareBar

42
components/ShareButton.js Normal file
View File

@@ -0,0 +1,42 @@
import React from 'react'
import ShareBar from '@/components/ShareBar'
/**
* 悬浮在屏幕右下角,分享按钮
* @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=' overflow-hidden '>
<div
className={
(popoverShow ? ' block ' : ' hidden ') +
' duration-200 transform transition z-50 font-normal'
}
>
<ShareBar post={post}/>
</div>
<div
ref={btnRef}
className='border dark:bg-black bg-white px-4 py-3 cursor-pointer text-md hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow'>
<div className='dark:text-gray-200 fa fa-share-alt' title='share' />
</div>
</div>
</div>
)
}
export default ShareButton

View File

@@ -0,0 +1,28 @@
import React from 'react'
const SocialButton = () => {
return <>
<div className='space-x-3 text-xl'>
<a className='fa fa-rss hover:underline' href='/feed' target='_blank' id='feed'/>
<a className='fa fa-info hover:underline mx-1' href='/article/about' id='about'/>
<a className='fa fa-github' target='_blank' rel='noreferrer' title={'github'}
href={'https://github.com/tangly1024'} />
<a className='fa fa-twitter' target='_blank' rel='noreferrer' title={'twitter'}
href={'https://twitter.com/troy1024_1'} />
<a className='fa fa-telegram' href={'https://t.me/tangly_1024'} title={'telegram'} />
<a className='fa fa-weibo' target='_blank' rel='noreferrer' title={'weibo'}
href={'http://weibo.com/tangly1024'} />
<span id='busuanzi_container_site_pv' className='hidden'><span className='s'> | </span>
<a href='https://www.cnzz.com/stat/website.php?web_id=1279970751' target='_blank'
id='busuanzi_container_site_pv'
className='fa fa-user' rel='noreferrer'> pv <span id='busuanzi_value_site_pv'></span></a>
</span>
<span id='busuanzi_container_site_uv' className='hidden'><span className='s'> | </span>
<a href='http://tongji.baidu.com/web/10000363165/overview/index?siteId=16809429' target='_blank'
className='fa fa-eye' rel='noreferrer'> uv <span id='busuanzi_value_site_uv'></span></a>
</span>
</div>
</>
}
export default SocialButton

14
components/TagItem.js Normal file
View File

@@ -0,0 +1,14 @@
import Link from 'next/link'
const TagItem = ({ tag }) => (
<Link href={`/tag/${encodeURIComponent(tag)}`}>
<a>
<p className="hover:shadow hover:scale-105 hover:bg-blue-500 bg-gray-200 hover:text-white duration-200 mr-1 px-2 py-1 leading-none text-sm
dark:bg-gray-500 dark:hover:bg-black">
{tag}
</p>
</a>
</Link>
)
export default TagItem

27
components/Tags.js Normal file
View File

@@ -0,0 +1,27 @@
import Link from 'next/link'
const Tags = ({ tags, currentTag }) => {
if (!tags) return <></>
return (
<ul className='flex flex-wrap py-1 max-w-full overflow-x-auto'>
{Object.keys(tags).map(key => {
const selected = key === currentTag
return (
<Link key={key} href={`/tag/${encodeURIComponent(key)}`}>
<li
className={`cursor-pointer hover:bg-gray-600 rounded-sm hover:text-white duration-200 mr-1 my-1 px-2 py-1 font-medium text-xs whitespace-nowrap
dark:text-gray-300 dark:hover:bg-gray-600 ${selected ? 'text-white bg-black dark:border-gray-600' : 'bg-gray-200 text-gray-600 dark:bg-gray-900 dark:border-gray-600'
}`}
>
<a>
{`${key} (${tags[key]})`}
</a>
</li>
</Link>
)
})}
</ul>
)
}
export default Tags

86
components/Toc.js Normal file
View File

@@ -0,0 +1,86 @@
import React from 'react'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import { cs } from 'react-notion-x'
/**
* 目录组件
*/
const Toc = ({ toc }) => {
// 无目录就直接返回空
if (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 = 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='text-center font-bold text-white'>
文章目录
</div>
<nav className='notion-table-of-contents text-gray-400 underline'>
{toc.map((tocItem) => {
const id = uuidToId(tocItem.id)
return (
<a
key={id}
href={`#${id}`}
className={cs(
'notion-table-of-contents-item',
`notion-table-of-contents-item-indent-level-${tocItem.indentLevel}`,
activeSection === id &&
' font-bold text-white'
)}
>
<span
className='notion-table-of-contents-item-body'
style={{
display: 'inline-block',
marginLeft: tocItem.indentLevel * 16
}}
>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</>
)
}
export default Toc

39
components/TopJumper.js Normal file
View File

@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react'
import throttle from 'lodash.throttle'
import { useLocale } from '@/lib/locale'
/**
* 跳转到网页顶部当屏幕下滑500像素后会出现该控件
* @returns {JSX.Element}
* @constructor
*/
const TopJumper = () => {
const locale = useLocale()
const [show, switchShow] = useState(false)
useEffect(() => {
const scrollListener = throttle(() => {
// 处理是否显示回到顶部按钮
const shouldShow = window.scrollY > 100
if (shouldShow !== show) {
switchShow(shouldShow)
}
}, 500)
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (
<div
className={(show ? 'animate__fadeInUp' : 'animate__fadeOutUp') + ' animate__animated animate__faster'}>
<div
className='border dark:bg-black bg-white cursor-pointer hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow-2xl hover:scale-125'
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
<a className='dark:text-gray-200 fa fa-arrow-up p-4' title={locale.POST.TOP}/>
</div>
</div>
)
}
export default TopJumper

88
components/TopNav.js Normal file
View File

@@ -0,0 +1,88 @@
import Link from 'next/link'
import BLOG from '@/blog.config'
import { useEffect, useState } from 'react'
import { useLocale } from '@/lib/locale'
import Router, { useRouter } from 'next/router'
import Tags from '@/components/Tags'
import localStorage from 'localStorage'
import { useTheme } from '@/lib/theme'
import DarkModeButton from '@/components/DarkModeButton'
import SocialButton from '@/components/SocialButton'
const TopNav = ({ tags, currentTag }) => {
const locale = useLocale()
const [hiddenMenu, switchHiddenMenu] = useState(!currentTag)
// 点击按钮更改菜单状态
const handleMenuClick = () => {
switchHiddenMenu(!hiddenMenu)
}
const router = useRouter()
const [searchValue, setSearchValue] = useState('')
const handleKeyUp = (e) => {
if (e.keyCode === 13) {
Router.push({ pathname: '/', query: { s: searchValue } })
}
}
return (
<div className='bg-white dark:bg-gray-600 block xl:hidden'>
{/* 隐藏的顶部菜单 */}
<div
className={(hiddenMenu ? 'h-0 ' : 'h-full ') + ' overflow-hidden bg-gray-800 text-xl text-gray-200 w-full transform ease-in-out duration-500'}>
<ul className='mx-5 duration-300'>
<li>
<div>
<Tags tags={tags} currentTag={currentTag} />
</div>
</li>
<li>
<SocialButton/>
</li>
</ul>
</div>
{/* 导航栏 */}
<div
id='sticky-nav'
className='text-sm ticky-nav m-auto w-full flex flex-row justify-between items-center px-5 pt-3 pb-2'
>
<div>
<Link href='/'>
<a
className='flex justify-center border-black border-2 bg-whitefont-semibold hover:bg-gray-800 hover:text-white p-2 duration-200
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black
'>{BLOG.title}</a>
</Link>
</div>
<div>
{/* 搜索框 */}
<div className='px-4 flex w-20'>
<i className='py-3 fa fa-search text-gray-400 absolute cursor-pointer px-2' />
<input
type='text'
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
className={'transition duration-200 leading-10 pl-8 block border-gray-300 dark:border-gray-600 bg-white text-black dark:bg-gray-800 dark:text-white'}
onKeyUp={handleKeyUp}
onChange={e => setSearchValue(e.target.value)}
defaultValue={router.query.s ?? ''}
/>
</div>
</div>
<div className='flex flex-nowrap space-x-1'>
<div onClick={handleMenuClick}
className='p-2.5 cursor-pointer text-gray-600 bg-white hover:bg-gray-800 hover:text-white duration-200
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black'>
<div className={'fa' + (hiddenMenu ? ' fa-bars ' : ' fa-times')} />
<span
className='px-0.5'>{hiddenMenu ? '' : ''}</span>
</div>
<DarkModeButton/>
</div>
</div>
</div>
)
}
export default TopNav

36
components/Utterances.js Normal file
View File

@@ -0,0 +1,36 @@
import BLOG from '@/blog.config'
import { useEffect } from 'react'
const Utterances = ({ issueTerm, layout }) => {
useEffect(() => {
const theme =
BLOG.appearance === 'auto'
? 'preferred-color-scheme'
: BLOG.appearance === 'light'
? 'github-light'
: 'github-dark'
const script = document.createElement('script')
const anchor = document.getElementById('comments')
script.setAttribute('src', 'https://utteranc.es/client.js')
script.setAttribute('crossorigin', 'anonymous')
script.setAttribute('async', true)
script.setAttribute('repo', BLOG.comment.utterancesConfig.repo)
script.setAttribute('issue-term', issueTerm)
script.setAttribute('theme', theme)
anchor.appendChild(script)
return () => {
anchor.innerHTML = ''
}
})
return (
<>
<div
id="comments"
className={layout && layout === 'fullWidth' ? '' : 'md:-ml-16'}
>
<div className="utterances-frame"></div>
</div>
</>
)
}
export default Utterances

41
components/Vercel.js Normal file

File diff suppressed because one or more lines are too long