Merge branch 'theme-Next' of https://github.com/tangly1024/NotionNext into theme-Next

This commit is contained in:
tangly1024
2022-01-08 22:26:58 +08:00
68 changed files with 966 additions and 613 deletions

View File

@@ -18,6 +18,7 @@
</p>
演示地址:[https://tangly1024.com/](https://tangly1024.com/)
## 亮点 ✨
**🚀 &nbsp;秒开,设备全适配**
@@ -44,12 +45,14 @@
**🕸 &nbsp;网址美观、搜索引擎优化**
## 更多特性
欢迎移步[我的博客](https://tangly1024.com/article/notion-next)查看
## 快速起步
- 给这个项目点个小星星 😉
- 将 [这个 Notion 模板](https://tanghh.notion.site/02ab3b8678004aa69e9e415905ef32a5) 制作副本,并分享这个页面给所有人
- [Fork](https://github.com/tangly1024/NotionNext/fork) 这个项目
-`blog.config.js` 配置相关选项
-`blog.config.js` 配置相关选项,更多关于配置的说明,请移步[NotionNext文档](https://docs.tangly1024.com/zh)
- _(可选)_ 用自己的图片替换 `/public` 文件夹里的 `avatar.jpg``favicon.svg``favicon.ico`
- 在 [Vercel](https://vercel.com) 上部署这个项目, 设定一下环境变量:
- `NOTION_PAGE_ID`: 你刚刚分享出去的 Notion 页面网址中的页面 ID通常是网址中工作区地址后的 32 位字符串

View File

@@ -1,52 +1,61 @@
const BLOG = {
title: '小唐笔记',
author: 'tangly1024',
email: 'tlyong1992@hotmail.com',
link: 'https://tangly1024.com',
description: '分享编程技术与记录生活',
headerStrings: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页文字
bannerImage: './bg_image.jpg', // 首图
title: '小唐笔记', // 站点标题
description: '分享编程技术与记录生活', // 站点描述
author: 'tangly1024', // 作者
bio: '一个普通的干饭人🍚', // 作者简介
email: 'tlyong1992@hotmail.com', // 联系邮箱
link: 'https://tangly1024.com', // 网站地址
keywords: ['Notion', '写作', '博客'], // 网站关键词
home: { // 首页
showHomeBanner: false, // 首页是否显示大图及标语 [true,false]
homeBannerStrings: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
homeBannerImage: './bg_image.jpg', // 背景图地址
showPostCover: false, // 文章列表显示封面图
showPreview: true, // 列表展示文章预览
showSummary: false // 显示用户自定义摘要
},
lang: 'zh-CN', // ['zh-CN','en-US'] default lang => see /lib/lang.js for more.
notionPageId: process.env.NOTION_PAGE_ID || 'bee1fccfa3bd47a1a7be83cc71372d83', // Important page_id
notionAccessToken: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
appearance: 'auto', // ['light', 'dark', 'auto'],
font: 'font-sans tracking-wider subpixel-antialiased', // 文章字体 ['font-sans', 'font-serif', 'font-mono'] @see https://www.tailwindcss.cn/docs/font-family
lightBackground: '#ffffff', // use hex value, don't forget '#' e.g #fffefc
font: 'font-serif tracking-wider subpixel-antialiased', // 文章字体 ['font-sans', 'font-serif', 'font-mono'] @see https://www.tailwindcss.cn/docs/font-family
lightBackground: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
darkBackground: '#111827', // use hex value, don't forget '#'
path: '', // leave this empty unless you want to deploy in a folder
since: 2020, // if leave this empty, current year will be used.
postListStyle: 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
postsPerPage: 6, // post counts per page
sortByDate: false,
showAbout: true, // WIP 是否显示关于
showArchive: true, // WIP 是否显示归档
autoCollapsedNavBar: false, // the automatically collapsed navigation bar
socialLink: {
topNavType: 'normal', // ['fixed','autoCollapse','normal'] 分别是固定顶部、固定底部滑动时自动折叠,不固定
menu: { // 菜单栏设置
showAbout: false, // 显示关于
showCategory: true, // 显示分类
showTag: true, // 显示标签
showArchive: true, // 显示归档
showSearch: true // 显示搜索
},
widget: { // 挂件及组件设置
showPet: false, // 是否显示宠物挂件
petLink: 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
showToTop: true, // 是否显示回顶
showToBottom: true, // 显示回底
showDarkMode: true, // 显示日间/夜间模式切换
showToc: true, // 移动端显示悬浮目录
showShareBar: false, // 文章分享功能
showRelatePosts: true, // 相关文章推荐
showCopyRight: true, // 文章版权声明
showLatestPost: false, // 右侧边栏显示最近更新
showCategoryList: false, // 右侧边栏显示文章分类列表
showTagList: false // 右侧边栏显示标签分类列表
},
socialLink: { // 社交链接,如不需要展示可以留空白,例如 weibo:''
weibo: 'https://weibo.com/tangly1024',
twitter: '',
twitter: 'https://twitter.com/troy1024_1',
github: 'https://github.com/tangly1024',
telegram: ''
telegram: 'https://t.me/tangly_1024'
},
seo: {
keywords: ['Notion', '写作', '博客'],
googleSiteVerification: '' // Remove the value or replace it with your own google site verification code
},
analytics: {
provider: 'ga', // Currently we support Google Analytics and Ackee, please fill with 'ga' or 'ackee', leave it empty to disable it.
ackeeConfig: {
tracker: '', // e.g 'https://ackee.tangly1024.net/tracker.js'
dataAckeeServer: '', // e.g https://ackee.tangly1024.net , don't end with a slash
domainId: '' // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
},
gaConfig: {
measurementId: 'G-68EK0W049N' // e.g: G-XXXXXXXXXX
},
baiduAnalytics: 'f683ef76f06bb187cbed5546f6f28f28', // e.g only need xxxxx -> https://hm.baidu.com/hm.js?[xxxxx]
busuanzi: true, // see http://busuanzi.ibruce.info/
cnzzAnalytics: '' // 站长统计id only need xxxxxxxx -> https://s9.cnzz.com/z_stat.php?id=[xxxxxxxx]&web_id=[xxxxxxx]
},
comment: {
// support provider: gitalk, utterances, cusdis
provider: 'cusdis', // leave it empty if you don't need any comment plugin
comment: { // 评论插件,支持 gitalk, utterances, cusdis
provider: '', // 不需要则留空白
gitalkConfig: {
repo: 'NotionNext', // The repository of store comments
owner: 'tangly1024',
@@ -62,14 +71,31 @@ const BLOG = {
},
utterancesConfig: {
repo: 'tangly1024/NotionNext'
},
gitter: '', // gitter聊天室
DaoVoiceId: '', // DaoVoice http://dashboard.daovoice.io/get-started
TidioId: '' // https://www.tidio.com/
},
// --- 高级设置
analytics: { // 文章访问量统计
busuanzi: true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
provider: 'ga', // 支持 Google Analytics and Ackee, please fill with 'ga' or 'ackee', leave it empty to disable it.
baiduAnalytics: 'f683ef76f06bb187cbed5546f6f28f28', // e.g only need xxxxx -> https://hm.baidu.com/hm.js?[xxxxx]
cnzzAnalytics: '', // 站长统计id only need xxxxxxxx -> https://s9.cnzz.com/z_stat.php?id=[xxxxxxxx]&web_id=[xxxxxxx]
gaConfig: {
measurementId: 'G-68EK0W049N' // e.g: G-XXXXXXXXXX
},
ackeeConfig: {
tracker: '', // e.g 'https://ackee.tangly1024.net/tracker.js'
dataAckeeServer: '', // e.g https://ackee.tangly1024.net , don't end with a slash
domainId: '' // e.g '0e2257a8-54d4-4847-91a1-0311ea48cc7b'
}
},
seo: {
googleSiteVerification: '' // Remove the value or replace it with your own google site verification code
},
googleAdsenseId: 'ca-pub-2708419466378217', // 谷歌广告ID
DaoVoiceId: '', // 在线聊天 DaoVoice http://dashboard.daovoice.io/get-started
TidioId: '', // 在线聊天 https://www.tidio.com/
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
showPet: true // 详情页是否显示宠物挂件
isProd: process.env.VERCEL_ENV === 'production'// distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
}
// export default BLOG
module.exports = BLOG

View File

@@ -1,11 +1,14 @@
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">
<div className="text-2xl mb-2">{locale.COMMON.COPYRIGHT}</div>
<ul className="text-sm dark:bg-gray-900 bg-gray-100 p-5 leading-8 border-l-4 border-red-500">
<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">
@@ -19,7 +22,8 @@ export default function ArticleCopyright ({ author, url }) {
</a>
</li>
<li>
{locale.COMMON.COPYRIGHT_NOTICE}
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
{locale.COMMON.COPYRIGHT_NOTICE}
</li>
</ul>
</section>

View File

@@ -19,7 +19,7 @@ import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { useRef } from 'react'
import { useEffect, useRef } from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import ArticleCopyright from './ArticleCopyright'
import Live2D from './Live2D'
@@ -30,7 +30,7 @@ import WordCount from './WordCount'
* @param {*} param0
* @returns
*/
export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, next }) {
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
const targetRef = useRef(null)
const drawerRight = useRef(null)
const url = BLOG.link + useRouter().asPath
@@ -43,26 +43,40 @@ export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, n
margin: getMediumZoomMargin()
})
const zoomRef = useRef(zoom ? zoom.clone() : null)
function attachZoom (image) {
if (zoomRef.current) {
(zoomRef.current).attach(image)
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])
}
}
}
const attachZoomRef = attachZoom
})
return (<>
<div id="article-wrapper" ref={targetRef} className="overflow-x-auto flex-grow max-w-5xl mx-auto w-screen md:w-full ">
<div id="container" ref={targetRef} 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="shadow md:hover:shadow-2xl duration-300 subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 xl:px-32 dark:border-gray-700 bg-white dark:bg-gray-800"
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 && (
{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} ref={attachZoomRef}src={post?.page_cover} className='object-center w-full' />
<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">
@@ -111,10 +125,9 @@ export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, n
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
{blockMap && (
{post.blockMap && (
<NotionRenderer
className={`${BLOG.font}`}
recordMap={blockMap}
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
@@ -137,12 +150,12 @@ export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, n
data-ad-slot="3806269138"></ins>
</section>
{/* 推荐文章 */}
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
{/* 版权声明 */}
<ArticleCopyright author={BLOG.author} url={url} />
{/* 推荐文章 */}
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
{/* 标签列表 */}
<section className="md:flex md:justify-between">
{post.tagItems && (
@@ -165,7 +178,7 @@ export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, n
</article>
{/* 评论互动 */}
<div className="mt-5 lg:px-40 md:hover:shadow-2xl duration-200 shadow w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-700">
<div className="lg:px-40 md:hover:shadow-2xl 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>
@@ -177,7 +190,6 @@ export default function ArticleDetail ({ post, blockMap, recommendPosts, prev, n
</div>
{/* 宠物 */}
{BLOG.showPet && <Live2D/>}
<Live2D/>
</>)

View File

@@ -3,24 +3,25 @@ import Link from 'next/link'
import React from 'react'
import Image from 'next/image'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolder } from '@fortawesome/free-solid-svg-icons'
import { faAngleDoubleRight, faFolder } from '@fortawesome/free-solid-svg-icons'
import TagItemMini from './TagItemMini'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import { useGlobal } from '@/lib/global'
const BlogPostCard = ({ post, tags }) => {
const BlogPostCard = ({ post, showSummary }) => {
const { locale } = useGlobal()
return (
<div key={post.id} className='shadow animate__animated animate__fadeIn flex xl:flex-row flex-col-reverse justify-between md:hover:shadow-xl duration-300
<div key={post.id} className='shadow border animate__animated animate__fadeIn flex flex-col-reverse justify-between md:hover:shadow-xl duration-300
w-full bg-white dark:bg-gray-800 dark:hover:bg-gray-700 dark:border-gray-600'>
<div className='p-8 flex flex-col justify-between w-full'>
<div className='lg:p-8 p-4 flex flex-col justify-between w-full'>
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<a className='cursor-pointer text-xl xl:text-2xl leading-tight text-black dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400'>
<a className='cursor-pointer font-bold text-3xl text-center leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400'>
{post.title}
</a>
</Link>
<p className='my-8 text-gray-700 dark:text-gray-300 text-md font-light leading-7 italic'>{post.summary}</p>
<div className='flex items-center justify-between flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 '>
<div className='flex mt-2 items-center justify-center 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'>
@@ -37,11 +38,36 @@ const BlogPostCard = ({ post, tags }) => {
</div>
</div>
{showSummary && <p className='mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
{post.summary}
</p>}
{BLOG.home?.showPreview && post?.blockMap && <div className='max-h-screen overflow-hidden truncate max-w-full'>
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
</div> }
<div className='border-b-2 w-full border-dashed py-2'></div>
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<div className='flex items-center cursor-pointer pt-6 justify-end leading-tight'>
<a className='bg-black p-2 text-white'>{locale.COMMON.ARTICLE_DETAIL}
<FontAwesomeIcon icon={faAngleDoubleRight} /></a>
</div>
</Link>
</div>
{post?.page_cover && (
{BLOG.home?.showPostCover && post?.page_cover && (
<Link href={`${BLOG.path}/article/${post.slug}`} passHref>
<div className='h-60 w-full xl:max-w-xs relative xl:h-full duration-200 cursor-pointer transform overflow-hidden'>
<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>
@@ -50,4 +76,8 @@ const BlogPostCard = ({ post, tags }) => {
)
}
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
export default BlogPostCard

View File

@@ -1,8 +1,6 @@
import BlogPostCard from '@/components/BlogPostCard'
import Pagination from '@/components/Pagination'
import PaginationNumber from './PaginationNumber'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import BlogPostListEmpty from '@/components/BlogPostListEmpty'
/**
@@ -13,58 +11,23 @@ import BlogPostListEmpty from '@/components/BlogPostListEmpty'
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListPage = ({ page = 1, posts = [], tags }) => {
let filteredBlogPosts = posts
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
const totalPage = Math.ceil(postCount / BLOG.postsPerPage)
// 处理查询过滤 支持标签、关键词过滤
let currentSearch = ''
const router = useRouter()
if (router.query && router.query.s) {
currentSearch = router.query.s
filteredBlogPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent + post.slug
return searchContent.toLowerCase().includes(currentSearch.toLowerCase())
})
}
// 处理分页
const totalPages = Math.ceil(filteredBlogPosts.length / BLOG.postsPerPage)
const postsToShow = filteredBlogPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
)
let showNext = false
if (filteredBlogPosts) {
const totalPosts = filteredBlogPosts.length
showNext = page * BLOG.postsPerPage < totalPosts
}
if (!postsToShow || postsToShow.length === 0) {
if (!posts || posts.length === 0) {
return <BlogPostListEmpty />
} else {
return <div id='post-list-wrapper' className='pt-16 md:pt-28 px-2 md:px-20'>
{(!page || page === 1) && (<div className='py-5' />)}
{(page && page !== 1) && (
<div className='pb-5'>
<div className='dark:text-gray-200 flex justify-between py-1'>
{page && page !== 1 && (<span> {page} / {totalPages}</span>)}
</div>
</div>
)}
<div>
return (
<div id="container">
{/* 文章列表 */}
<div className='flex flex-wrap'>
{postsToShow.map(post => (
<BlogPostCard key={post.id} post={post} tags={tags} />
<div className="flex flex-wrap lg:space-y-4 space-y-1">
{posts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
<Pagination page={page} showNext={showNext} />
<PaginationNumber page={page} totalPage={totalPage} />
</div>
</div>
)
}
}

View File

@@ -1,10 +1,9 @@
import BlogPostCard from '@/components/BlogPostCard'
import BLOG from '@/blog.config'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import throttle from 'lodash.throttle'
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'
/**
* 博客列表滚动分页
@@ -14,7 +13,7 @@ import { useGlobal } from '@/lib/global'
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListScroll = ({ posts = [], tags, currentSearch, currentCategory, currentTag }) => {
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = BLOG.home.showSummary }) => {
const postsPerPage = BLOG.postsPerPage
const [page, updatePage] = useState(1)
const postsToShow = getPostByPage(page, posts, postsPerPage)
@@ -53,12 +52,12 @@ const BlogPostListScroll = ({ posts = [], tags, currentSearch, currentCategory,
if (!postsToShow || postsToShow.length === 0) {
return <BlogPostListEmpty currentSearch={currentSearch} />
} else {
return <div id='post-list-wrapper' className='mt-10 md:mt-0' ref={targetRef}>
return <div id='container' ref={targetRef}>
{/* 文章列表 */}
<div className='flex flex-wrap space-y-8 mx-5 md:mx-0'>
<div className='flex flex-wrap space-y-8'>
{postsToShow.map(post => (
<BlogPostCard key={post.id} post={post} tags={tags}/>
<BlogPostCard key={post.id} post={post} showSummary={showSummary}/>
))}
</div>

View File

@@ -1,7 +1,7 @@
import Link from 'next/link'
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolder, faFolderOpen, faThList } from '@fortawesome/free-solid-svg-icons'
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { useGlobal } from '@/lib/global'
const CategoryList = ({ currentCategory, categories }) => {
@@ -11,7 +11,7 @@ const CategoryList = ({ currentCategory, categories }) => {
const { locale } = useGlobal()
return <ul className='flex py-1 space-x-3'>
<li className='w-16 py-2 dark:text-gray-200 whitespace-nowrap'><FontAwesomeIcon className='mr-2' icon={faThList} />{locale.COMMON.CATEGORY}</li>
<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 (

View File

@@ -26,42 +26,46 @@ const Comment = ({ frontMatter }) => {
const router = useRouter()
const { theme } = useGlobal()
return <div className='comment text-gray-800 dark:text-gray-300'>
{BLOG.comment.provider === 'gitalk' && (<div className='m-10'>
<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
}}
/>
</div>)}
{BLOG.comment.provider === 'utterances' && (<div className='m-10'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
)}
{BLOG.comment.provider === 'cusdis' && (<>
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
<div className='m-10'>
<CusdisComponent
attrs={{
host: BLOG.comment.cusdisConfig.host,
appId: BLOG.comment.cusdisConfig.appId,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.link + router.asPath,
theme: theme
}}
lang={BLOG.lang.toLowerCase()}
/>
return (
BLOG.comment.provider !== '' && (
<div className='comment mt-5 text-gray-800 dark:text-gray-300'>
{BLOG.comment.provider === 'gitalk' && (<div className='m-10'>
<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
}}
/>
</div>)}
{BLOG.comment.provider === 'utterances' && (<div className='m-10'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
)}
{BLOG.comment.provider === 'cusdis' && (<>
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
<div className='m-10'>
<CusdisComponent
attrs={{
host: BLOG.comment.cusdisConfig.host,
appId: BLOG.comment.cusdisConfig.appId,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.link + router.asPath,
theme: theme
}}
lang={BLOG.lang.toLowerCase()}
/>
</div>
</>)}
</div>
</>)}
</div>
)
)
}
export default Comment

View File

@@ -9,7 +9,7 @@ const CommonHead = ({ meta }) => {
const title = meta?.title || BLOG.title
const description = meta?.description || BLOG.description
const type = meta?.type || 'website'
const keywords = meta?.tags || BLOG.seo.keywords
const keywords = meta?.tags || BLOG.keywords
return <Head>
<title>{title}</title>

View File

@@ -1,10 +1,13 @@
import { useEffect, useState } from 'react'
import { useGlobal } from '@/lib/global'
import { loadUserThemeFromCookies, saveTheme, useGlobal } from '@/lib/global'
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { loadUserThemeFromCookies, saveTheme } from '@/lib/theme'
import BLOG from '@/blog.config'
export default function FloatDarkModeButton () {
if (!BLOG.widget?.showDarkMode) {
return <></>
}
const [show, switchShow] = useState(false)
const scrollListener = () => {
const scrollY = window.pageYOffset
@@ -14,9 +17,10 @@ export default function FloatDarkModeButton () {
}
}
useEffect(() => {
scrollListener()
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
})
}, [show])
const { changeTheme } = useGlobal()
const userTheme = loadUserThemeFromCookies()
@@ -32,11 +36,12 @@ export default function FloatDarkModeButton () {
return (
<div
id='float-dark-mode-button'
onClick={handleChangeDarkMode}
className={
(show ? '' : 'hidden lg:block') +
' animate__fadeInRight px-3.5 py-3 animate__animated animate__faster shadow-card fixed right-3 bottom-24 z-10 duration-200 text-xs cursor-pointer rounded-xl' +
' text-black shadow-card dark:border-gray-500 glassmorphism dark:bg-gray-700 dark:text-gray-200'
(show ? '' : ' hidden ') +
' animate__fadeInRight animate__animated animate__faster fixed right-1 bottom-28 z-10 duration-500 text-xs cursor-pointer ' +
' text-black dark:border-gray-500 flex justify-center items-center w-8 h-8 glassmorphism dark:bg-gray-700 dark:text-gray-200'
}
>
<FontAwesomeIcon

View File

@@ -9,9 +9,9 @@ const Footer = ({ title }) => {
const y = d.getFullYear()
return (
<footer
className='bg-gray-800 dark:bg-black dark:border-gray-900 border-t flex-shrink-0 justify-center text-center m-auto w-full text-gray-400 text-sm p-6'
className='dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full text-gray-400 text-sm p-6'
>
<FontAwesomeIcon icon={faCopyright} /> {` ${y}`} <span> <a href={BLOG.link} className='underline font-bold text-gray-100'>{BLOG.author}</a>. Powered by <a href='https://notion.so' className='underline font-bold text-gray-100'>Notion</a> & <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-100'>NotionNext</a>.</span>
<FontAwesomeIcon icon={faCopyright} /> {` ${y}`} <span> <a href={BLOG.link} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.author}</a>. 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>
<br />
<FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='ml-1 font-bold'>闽ICP备20010331号</a>
<span > <Link href='/article/privacy-policy' ><a className='ml-1 mr-1 font-bold underline'>隐私政策</a></Link></span>

View File

@@ -19,7 +19,7 @@ export default function Header () {
if (!typed && window && document.getElementById('typed')) {
changeType(
new Typed('#typed', {
strings: BLOG.headerStrings,
strings: BLOG.home.homeBannerStrings,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
@@ -99,7 +99,7 @@ export default function 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.bannerImage}")`
`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">

View File

@@ -9,7 +9,7 @@ const InfoCard = ({ postCount }) => {
<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.title}
alt={BLOG.author}
width={120}
height={120}
loading='lazy'
@@ -17,8 +17,8 @@ const InfoCard = ({ postCount }) => {
className='rounded-full'
/>
</div>
<div className='text-3xl font-serif dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.title}</div>
<div className='font-light dark:text-white py-2 hover:scale-105 transform duration-200'>{BLOG.description}</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>
</>

View File

@@ -1,3 +1,4 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -12,13 +13,17 @@ import smoothscroll from 'smoothscroll-polyfill'
* @returns {JSX.Element}
* @constructor
*/
const JumpToBottomButton = ({ targetRef, showPercent = false }) => {
const JumpToBottomButton = ({ showPercent = false }) => {
if (!BLOG.widget?.showToBottom) {
return <></>
}
const { locale } = useGlobal()
const [show, switchShow] = useState(false)
const [percent, changePercent] = useState(0)
const scrollListener = () => {
// 处理是否显示回到顶部按钮
const clientHeight = targetRef ? (targetRef.current ? targetRef.current.clientHeight : 0) : 0
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))
@@ -29,6 +34,12 @@ const JumpToBottomButton = ({ targetRef, showPercent = false }) => {
}
changePercent(per)
}
function scrollToBottom () {
const targetRef = document.getElementById('wrapper')
window.scrollTo({ top: targetRef.clientHeight, behavior: 'smooth' })
}
useEffect(() => {
smoothscroll.polyfill()
@@ -36,15 +47,13 @@ const JumpToBottomButton = ({ targetRef, showPercent = false }) => {
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<div id='jump-to-top' className='right-3 fixed flex bottom-36 duration-500 z-20'>
<div onClick={() => window.scrollTo({ top: targetRef.current.clientHeight, behavior: 'smooth' })}
className={(show ? '' : 'hidden') + ' animate__fadeInRight animate__animated animate__faster shadow-card rounded-xl glassmorphism py-3 cursor-pointer '}>
<div className='text-center'>
<div className='w-10 dark:text-gray-200 transform hover:scale-150 duration-200 text-xs' title={locale.POST.TOP} >
return (<div id='jump-to-top' className='right-1 fixed flex bottom-36 z-20'>
<div onClick={() => scrollToBottom()}
className={(show ? '' : 'hidden') + ' animate__fadeInRight duration-500 animate__animated animate__faster glassmorphism flex justify-center items-center w-8 h-8 cursor-pointer '}>
<div className='dark:text-gray-200 transform hover:scale-150 text-xs duration-200' title={locale.POST.TOP} >
<FontAwesomeIcon icon={faArrowDown} />
</div>
{showPercent && (<div className='w-10 text-xs dark:text-gray-200'>{percent}</div>)}
</div>
</div>
</div>)
}

View File

@@ -3,6 +3,7 @@ import { useGlobal } from '@/lib/global'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons'
import smoothscroll from 'smoothscroll-polyfill'
import BLOG from '@/blog.config'
/**
* 跳转到网页顶部
@@ -12,18 +13,20 @@ import smoothscroll from 'smoothscroll-polyfill'
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = ({ targetRef, showPercent = false }) => {
const JumpToTopButton = ({ showPercent = false }) => {
if (!BLOG.widget?.showToTop) {
return <></>
}
const { locale } = useGlobal()
const [show, switchShow] = useState(false)
const [percent, changePercent] = useState(0)
const scrollListener = () => {
// 处理是否显示回到顶部按钮
const clientHeight = targetRef ? (targetRef.current ? targetRef.current.clientHeight : 0) : 0
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 && scrollY < windowTop
const shouldShow = scrollY > 100 && per > 0
if (shouldShow !== show) {
@@ -37,15 +40,13 @@ const JumpToTopButton = ({ targetRef, showPercent = false }) => {
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<div id='jump-to-top' className='right-3 fixed flex bottom-48 duration-500 z-20'>
return (<div id='jump-to-top' className='right-1 fixed flex bottom-44 z-20'>
<div onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className={(show ? '' : 'hidden') + ' animate__fadeInRight animate__animated animate__faster shadow-card rounded-xl glassmorphism py-3 cursor-pointer '}>
<div className='text-center'>
<div className='w-10 dark:text-gray-200 transform hover:scale-150 duration-200 text-xs' title={locale.POST.TOP} >
className={(show ? '' : 'hidden') + ' animate__fadeInRight duration-500 animate__animated animate__faster flex justify-center items-center w-8 h-8 glassmorphism cursor-pointer '}>
<div className='dark:text-gray-200 transform hover:scale-150 text-xs duration-200' title={locale.POST.TOP} >
<FontAwesomeIcon icon={faArrowUp} />
</div>
{showPercent && (<div className='w-10 text-xs dark:text-gray-200'>{percent}</div>)}
</div>
</div>
</div>)
}

View File

@@ -1,5 +1,6 @@
import BLOG from '@/blog.config'
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
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'
@@ -10,25 +11,19 @@ import { useRouter } from 'next/router'
* @param sliceCount 截取展示的数量 默认6
* @constructor
*/
const LatestPostsGroup = ({ posts, sliceCount = 5 }) => {
// 深拷贝
let postsSortByDate = Object.create(posts)
// 时间排序
postsSortByDate.sort((a, b) => {
const dateA = new Date(a?.lastEditedTime || a.createdTime)
const dateB = new Date(b?.lastEditedTime || b.createdTime)
return dateB - dateA
})
// 只取前五
postsSortByDate = postsSortByDate.slice(0, sliceCount)
const LatestPostsGroup = ({ posts }) => {
if (!posts) {
return <></>
}
// 获取当前路径
const currentPath = useRouter().asPath
const { locale } = useGlobal()
return <>
{postsSortByDate.map(post => {
return <section className='mt-8'>
<div className='text-sm pb-4 px-5 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>
@@ -42,6 +37,6 @@ const LatestPostsGroup = ({ posts, sliceCount = 5 }) => {
</Link>
)
})}
</>
</section>
}
export default LatestPostsGroup

View File

@@ -1,15 +1,18 @@
/* eslint-disable no-undef */
import { useEffect } from 'react'
import BLOG from '@/blog.config'
let hasLoad = false
export default function Live2D () {
useEffect(() => {
if (window && !hasLoad) {
initLive2D()
hasLoad = true
}
})
return <div className='fixed right-0 bottom-0 hidden md:block lg:mr-24 2xl:mr-48 z-20'>
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>
}
@@ -22,7 +25,7 @@ function initLive2D () {
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
]).then(() => {
// https://github.com/xiazeyu/live2d-widget-models
loadlive2d('live2d', 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json')
loadlive2d('live2d', BLOG.widget.petLink)
})
}
}

View File

@@ -4,10 +4,10 @@ import React from 'react'
const Logo = () => {
return <Link href='/' passHref>
<div title={BLOG.title} className='mx-auto border dark:border-gray-600 text-center cursor-pointer text-xl dark:text-gray-300 font-semibold dark:hover:bg-gray-600 text-white p-2 hover:scale-105 hover:shadow-2xl duration-200 transform'>
<span className='text-red-600'>Tangly</span>
<span className='text-blue-400'>1024</span>
</div>
<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

View File

@@ -3,7 +3,7 @@ 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, faThList } from '@fortawesome/free-solid-svg-icons'
import { faArchive, faHome, faTag, faTh, faUser } from '@fortawesome/free-solid-svg-icons'
import BLOG from 'blog.config'
const MenuButtonGroup = ({ allowCollapse = false }) => {
@@ -11,27 +11,18 @@ const MenuButtonGroup = ({ allowCollapse = false }) => {
const router = useRouter()
const links = [
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ id: 1, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', show: BLOG.showArchive },
{ id: 2, icon: faThList, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.showArchive },
{ id: 3, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.showArchive }
// { id: 2, icon: faInfoCircle, name: locale.NAV.ABOUT, to: '/about', show: BLOG.showAbout }
// { id: 7, icon: 'faGithub', name: 'Github', to: 'https://github.com/tangly1024', show: true },
// { id: 5, icon: 'faWeibo', name: '微博', to: 'https://weibo.com/tangly1024', show: true },
// { id: 4, icon: 'faEnvelope', name: locale.NAV.MAIL, to: 'mailto:tlyong1992@hotmail.com', show: true }
// { id: 2, icon: 'faRssSquare', name: locale.NAV.RSS, to: '/feed', show: true },
// { id: 3, icon: 'faCompass', name: '发现', to: 'https://search.tangly1024.com/', show: true }
// { id: 6, icon: 'faMapMarker', name: 'Fuzhou', to: '#', show: true },
// { id: 8, icon: 'faTwitter', name: 'Twitter', to: 'https://twitter.com/troy1024_1', show: true },
// { id: 9, icon: 'faTelegram', name: 'Telegram', to: 'https://t.me/tangly_1024', show: true }
{ id: 1, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', show: BLOG.menu.showArchive },
{ id: 2, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: BLOG.menu.showCategory },
{ id: 3, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: BLOG.menu.showTag },
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: BLOG.menu.showAbout }
]
return <nav id='nav'>
<div className='leading-8 text-gray-500 dark:text-gray-400 '>
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 '>
{links.map(link => {
if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <Link key={link.id + link.icon} title={link.to} href={link.to} >
<a className={'py-1 my-1 px-5 mx-2 duration-300 text-base hover:bg-gray-500 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
(selected ? 'bg-gray-700 text-white ' : ' ')} >
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
<a className={'py-1 my-1 px-5 mx-2 duration-300 text-base 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 justify-center flex '>
<FontAwesomeIcon icon={link.icon} />
</div>
@@ -39,10 +30,9 @@ const MenuButtonGroup = ({ allowCollapse = false }) => {
</a>
</Link>
} else {
return <></>
return null
}
})}
</div>
</nav>
</nav>
}
export default MenuButtonGroup

View File

@@ -0,0 +1,94 @@
import BLOG from '@/blog.config'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 数字翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const PaginationNumber = ({ page, totalPage }) => {
const router = useRouter()
const currentPage = +page
const showNext = page !== totalPage
const pages = generatePages(page, currentPage, totalPage)
return (
<div className='my-5 flex justify-center items-end font-medium text-black 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

View File

@@ -4,13 +4,13 @@ import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
/**
* 翻页插件
* 简易翻页插件
* @param page 当前页码
* @param showNext 是否有下一页
* @returns {JSX.Element}
* @constructor
*/
const Pagination = ({ page, showNext }) => {
const PaginationSimple = ({ page, showNext }) => {
const { locale } = useGlobal()
const router = useRouter()
const currentPage = +page
@@ -39,4 +39,4 @@ const Pagination = ({ page, showNext }) => {
)
}
export default Pagination
export default PaginationSimple

View File

@@ -14,6 +14,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
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)
}
}

View File

@@ -1,24 +1,25 @@
import React from 'react'
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
/**
* 展示文章推荐
*/
const RecommendPosts = ({ recommendPosts }) => {
if (!recommendPosts || recommendPosts.length < 1) {
if (!BLOG.widget?.showRelatePosts || !recommendPosts || recommendPosts.length < 1) {
return <></>
}
const { locale } = useGlobal()
return (
<div className="dark:text-gray-300 pt-2">
<div className="mb-2 text-2xl">{locale.COMMON.RELATE_POSTS}</div>
<ul className="list-disc pl-6 text-sm dark:bg-gray-900 bg-gray-100 p-2 my-2 border-l-4 border-yellow-500">
<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:text-blue-500 hover:underline">
<a className="cursor-pointer hover:underline">
{post.title}
</a>
</Link>

View File

@@ -21,7 +21,7 @@ const SearchDrawer = ({ cRef }) => {
return (
<div id='search-drawer-wrapper' ref={searchDrawer} className='hidden'>
<div className='flex absolute px-5 w-full h-full left-0 top-14 z-50 justify-center'>
<div className='md:max-w-3xl w-full mx-auto'>
<div className='md:max-w-3xl w-full mx-auto animate__animated animate__faster animate__fadeIn'>
<SearchInput cRef={searchInputRef} />
</div>
</div>

View File

@@ -17,6 +17,9 @@ import {
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

View File

@@ -6,6 +6,8 @@ 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'
/**
* 侧边平铺
@@ -19,22 +21,26 @@ import Tabs from '@/components/Tabs'
* @returns {JSX.Element}
* @constructor
*/
const SideAreaLeft = ({ title, tags, currentTag, post, posts, categories, currentCategory, currentSearch, targetRef }) => {
const SideAreaLeft = ({ title, tags, currentTag, post, postCount, categories, currentCategory, currentSearch, targetRef }) => {
const { locale } = useGlobal()
const showToc = post && post.toc && post.toc.length > 1
const postCount = posts?.length || 0
return <>
<section className='sticky top-8 w-60'>
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 py-4 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
<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'>
<MenuButtonGroup allowCollapse={true} />
<div className='px-5 pt-2'>
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
</div>
{BLOG.menu.showSearch && <div className='px-5 pt-2'>
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
</div>}
</section>
</section>
<section 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'>
@@ -43,13 +49,12 @@ const SideAreaLeft = ({ title, tags, currentTag, post, posts, categories, curren
)}
<div key={locale.NAV.ABOUT} className='mb-5 bg-white dark:bg-gray-800 duration-200 py-6'>
<InfoCard postCount={postCount} />
<InfoCard />
<Analytics postCount={postCount}/>
</div>
</Tabs>
</section>
</>
</aside>
}
export default SideAreaLeft

View File

@@ -1,8 +1,11 @@
import LatestPostsGroup from '@/components/LatestPostsGroup'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import { faArchive } from '@fortawesome/free-solid-svg-icons'
import { faAngleDoubleRight, faAngleRight, faTags, faThList } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
import CategoryGroup from './CategoryGroup'
import TagGroups from './TagGroups'
/**
* 侧边平铺
@@ -21,30 +24,19 @@ const SideAreaRight = ({
tags,
currentTag,
post,
posts,
slot,
categories,
currentCategory,
currentSearch,
targetRef
}) => {
const { locale } = useGlobal()
// const postCount = posts?.length || 0
// const showToc = post && post.toc && post.toc.length > 1
const { widget } = BLOG
if (!widget?.showCategoryList && !widget.showTagList && !widget.showLatestPost) {
return <></>
}
return (
<>
{/* <section className='hidden lg:block mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200 py-8 '>
<InfoCard postCount={postCount} />
</section> */}
{/* 菜单 */}
{/* <section className='hidden lg:block mb-5 py-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
<MenuButtonGroup allowCollapse={true} />
<div className='px-5 pt-2'>
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
</div>
</section> */}
return (<aside id='right' className='hidden 2xl:block flex-col w-60 ml-4'>
<section className="shadow mb-5 py-4 px-2 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
{/* 展示广告 */}
@@ -59,53 +51,26 @@ const SideAreaRight = ({
></ins>
</section>
{/* <Analytics postCount={postCount}/> */}
<div className="sticky top-8">
{/* {showToc && (
<section className='pb-10 mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
<div className='border-b text-center text-2xl bg-white text-black dark:border-gray-700 dark:bg-gray-700 dark:text-white py-6 px-6'>
{locale.COMMON.TABLE_OF_CONTENTS}
</div>
<Toc toc={post.toc} targetRef={targetRef} />
</section>
)} */}
{/* 分类 */}
{/* {categories && (
{widget?.showCategoryList && categories && (
<section className='shadow py-4 mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
<div className='text-sm px-5 mb-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={faAngleDoubleRight} />
{locale.COMMON.MORE} <FontAwesomeIcon icon={faAngleRight} />
</a>
</Link>
</div>
<CategoryGroup currentCategory={currentCategory} categories={categories} />
</section>
)} */}
{/* 最新文章 */}
{posts && (
<section className="shadow py-4 mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
<div className="text-sm pb-2 px-5 flex flex-nowrap justify-between">
<div className="font-light text-gray-600 dark:text-gray-300">
<FontAwesomeIcon icon={faArchive} className="mr-2" />
{locale.COMMON.LATEST_POSTS}
<span className='text-red-500 text-xs ml-1'>NEW</span>
</div>
</div>
<LatestPostsGroup posts={posts} />
</section>
)}
{/* <section className="shadow py-4 px-5 mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
<SearchInput currentTag={currentTag} currentSearch={currentSearch}/>
</section> */}
{slot}
{/* 标签云 */}
{/* {tags && (
{widget?.showTagList && tags && (
<section className="shadow py-4 mb-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200">
<div className="text-sm pb-1 px-5 flex flex-nowrap justify-between font-light dark:text-gray-200">
<div className="text-gray-600 dark:text-gray-200">
@@ -123,10 +88,10 @@ const SideAreaRight = ({
<TagGroups tags={tags} currentTag={currentTag} />
</div>
</section>
)} */}
)}
</div>
</>
</aside>
)
}
export default SideAreaRight

View File

@@ -1,14 +1,11 @@
import React from 'react'
import MenuButtonGroup from '@/components/MenuButtonGroup'
import CategoryGroup from '@/components/CategoryGroup'
import InfoCard from '@/components/InfoCard'
import TagGroups from '@/components/TagGroups'
import LatestPostsGroup from '@/components/LatestPostsGroup'
import CategoryGroup from '@/components/CategoryGroup'
import SearchInput from '@/components/SearchInput'
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import { faAngleDoubleRight, faTag, faThList } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleDoubleRight, faArchive, faTags, faThList } from '@fortawesome/free-solid-svg-icons'
import Link from 'next/link'
import React from 'react'
/**
* 侧边栏
@@ -22,32 +19,16 @@ import { faAngleDoubleRight, faArchive, faTags, faThList } from '@fortawesome/fr
* @returns {JSX.Element}
* @constructor
*/
const SideBar = ({ title, tags, currentTag, post, posts, categories, currentCategory, currentSearch }) => {
const SideBar = ({ title, tags, currentTag, post, slot, categories, currentCategory, currentSearch }) => {
const { locale } = useGlobal()
return <aside id='sidebar' className='pt-5 bg-white dark:bg-gray-900 w-80 z-10 dark:border-gray-500 border-gray-200 scroll-hidden h-full'>
<InfoCard />
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='hidden lg:block'>
<MenuButtonGroup allowCollapse={true} />
<section className='py-5'>
<InfoCard />
</section>
<section className='p-5'>
<SearchInput currentTag={currentTag} currentSearch={currentSearch} />
</section>
{/* 最新文章 */}
{posts && (
<section className='mt-4'>
<div className='text-sm pb-4 px-5 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>
<LatestPostsGroup posts={posts} />
</section>
)}
{/* 分类 */}
{categories && (
<section className='mt-8'>
@@ -65,9 +46,9 @@ const SideBar = ({ title, tags, currentTag, post, posts, categories, currentCate
{/* 标签云 */}
{tags && (
<section className='mt-8'>
<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={faTags} className='mr-2'/>{locale.COMMON.TAGS}</div>
<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} />
@@ -79,6 +60,9 @@ const SideBar = ({ title, tags, currentTag, post, posts, categories, currentCate
</div>
</section>
)}
{slot}
</div>
<section className='bg-white dark:bg-gray-900'>

View File

@@ -1,16 +1,17 @@
import SideBar from '@/components/SideBar'
import React, { useEffect, useImperativeHandle, useState } from 'react'
import { useRouter } from 'next/router'
import React, { useEffect, useImperativeHandle } from 'react'
/**
* 侧边栏抽屉面板,可以从侧面拉出
* @returns {JSX.Element}
* @constructor
*/
const SideBarDrawer = ({ post, currentTag, cRef, tags, posts, categories, currentCategory }) => {
const SideBarDrawer = ({ post, currentTag, cRef, tags, slot, categories, currentCategory }) => {
// 暴露给父组件 通过cRef.current.handleMenuClick 调用
useImperativeHandle(cRef, () => {
return {
handleSwitchSideDrawerVisible: () => switchSideDrawerVisible()
handleSwitchSideDrawerVisible: () => switchSideDrawerVisible(true)
}
})
@@ -19,11 +20,19 @@ const SideBarDrawer = ({ post, currentTag, cRef, tags, posts, categories, curren
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 [isShow, changeHiddenStatus] = useState(false)
const switchSideDrawerVisible = () => {
const showStatus = !isShow
changeHiddenStatus(showStatus)
const switchSideDrawerVisible = (showStatus) => {
if (window) {
const sideBarDrawer = window.document.getElementById('sidebar-drawer')
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
@@ -40,10 +49,10 @@ const SideBarDrawer = ({ post, currentTag, cRef, tags, posts, categories, curren
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} posts={posts} categories={categories} currentCategory={currentCategory} />
<SideBar tags={tags} post={post} slot={slot} categories={categories} currentCategory={currentCategory} />
</div>
{/* 背景蒙版 */}
<div id='sidebar-drawer-background' onClick={switchSideDrawerVisible} className='hidden fixed top-0 left-0 z-30 w-full h-full bg-black bg-opacity-30'/>
<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>
}

View File

@@ -10,8 +10,8 @@ import React from 'react'
* @constructor
*/
const SocialButton = () => {
return <div className='w-52 justify-center flex'>
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 px-6'>
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>}

View File

@@ -36,7 +36,7 @@ const StickyBar = ({ children }) => {
return (
<div id='sticky-bar' className='sticky flex-grow justify-center top-14 md:top-0 duration-500 z-10 pb-16'>
<div className='glassmorphism dark:border-gray-600 px-5 absolute rounded-none md:rounded-xl shadow-xl border w-full hidden-scroll'>
<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>

View File

@@ -18,7 +18,7 @@ const Tabs = ({ children }) => {
})
if (count === 1) {
return <section className='shadow'>
return <section className='shadow hover:shadow-xl duration-200'>
{children}
</section>
}
@@ -30,16 +30,16 @@ const Tabs = ({ children }) => {
return (
<section >
{<div className='shadow hidden lg:block mb-5 py-4 px-5 bg-white dark:bg-gray-800 hover:shadow-xl duration-200'>
{<div className='shadow hidden lg:block mb-5 py-4 px-5 bg-white dark:bg-gray-800 hover:shadow-xl 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' : 'font-extralight cursor-pointer'} onClick={() => { tabClickHandle(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'} onClick={() => { tabClickHandle(index) }}>
{item?.key}
</li>
})}
</ul>
{children.map((item, index) => {
return <section key={index} className={`${currentTab === index ? 'block' : 'hidden'}`}>
return <section key={index} className={`${currentTab === index ? 'block animate__animated animate__fadeIn animate__faster' : 'hidden'}`}>
{item}
</section>
})}

View File

@@ -12,12 +12,12 @@ const TagItem = ({ tag, selected }) => {
return (
<Link href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
<li
className={`notion-${tag.color}_background list-none cursor-pointer rounded-md
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
text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white`}>
<a>
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})` : ''}
</a>
</div>
</li>
</Link>
)

View File

@@ -1,8 +1,5 @@
import React from 'react'
import TagItem from '@/components/TagItem'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTags } from '@fortawesome/free-solid-svg-icons'
/**
* 横向的标签列表
* @param tags
@@ -15,7 +12,7 @@ const TagList = ({ tags, currentTag }) => {
return <></>
}
return <ul className='flex py-1 space-x-3'>
<li className='w-20 py-2 dark:text-gray-200 whitespace-nowrap'><FontAwesomeIcon icon={faTags} className='mr-2'/>标签:</li>
<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}/>

View File

@@ -7,7 +7,7 @@ import BLOG from '@/blog.config'
*/
const ThirdPartyScript = () => {
return (<>
{BLOG.DaoVoiceId && (<>
{BLOG.comment?.DaoVoiceId && (<>
{/* DaoVoice 反馈 */}
<script async dangerouslySetInnerHTML={{
__html: `
@@ -18,7 +18,7 @@ const ThirdPartyScript = () => {
<script async dangerouslySetInnerHTML={{
__html: `
daovoice('init', {
app_id: "${BLOG.DaoVoiceId}"
app_id: "${BLOG.comment.DaoVoiceId}"
});
daovoice('update');
`
@@ -30,13 +30,25 @@ const ThirdPartyScript = () => {
{BLOG.googleAdsenseId && (<script data-ad-client={BLOG.googleAdsenseId} async
src='https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'/>)}
{BLOG.TidioId && (<>
{BLOG.comment?.TidioId && (<>
{/* Tidio在线反馈 */}
<script async
src={`//code.tidio.co/${BLOG.TidioId}.js`}
src={`//code.tidio.co/${BLOG.comment.TidioId}.js`}
/>
</>)}
{/* */}
{BLOG.gitter && (<>
<script async dangerouslySetInnerHTML={{
__html: `
((window.gitter = {}).chat = {}).options = {
room: 'tangly1024/community'
};
`
}}/>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
</>)}
{/* 代码统计 */}
{BLOG.isProd && (<>

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'
import React from 'react'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import Progress from './Progress'
@@ -12,8 +12,9 @@ import Progress from './Progress'
*/
const Toc = ({ toc, targetRef }) => {
// 无目录就直接返回空
if (!toc || toc.length < 1) return <></>
if (!toc || toc.length < 1) {
return <></>
}
// 监听滚动事件
React.useEffect(() => {
window.addEventListener('scroll', actionSectionScrollSpy)
@@ -26,7 +27,7 @@ const Toc = ({ toc, targetRef }) => {
// 同步选中目录事件
const [activeSection, setActiveSection] = React.useState(null)
const throttleMs = 100
const actionSectionScrollSpy = useCallback(throttle(() => {
const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h')
let prevBBox = null
let currentSectionId = activeSection
@@ -63,14 +64,14 @@ const Toc = ({ toc, targetRef }) => {
key={id}
href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light
notion-table-of-contents-item-indent-level-${tocItem.indentLevel}
${activeSection === id && ' font-bold text-gray-600 dark:text-gray-300'}`}
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>

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
import { useGlobal } from '@/lib/global'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faListOl } from '@fortawesome/free-solid-svg-icons'
import BLOG from '@/blog.config'
/**
* 点击召唤目录抽屉
@@ -11,6 +12,9 @@ import { faListOl } from '@fortawesome/free-solid-svg-icons'
* @constructor
*/
const TocDrawerButton = (props) => {
if (!BLOG.widget?.showToc) {
return <></>
}
const { locale } = useGlobal()
const [show, switchShow] = useState(false)
const scrollListener = () => {
@@ -27,10 +31,10 @@ const TocDrawerButton = (props) => {
})
return (
<div id='toc-drawer-button' className='right-3 fixed bottom-60 duration-500 z-20'>
<div onClick={props.onClick} className={(show ? 'animate__fadeInRight' : 'hidden') + ' py-3 px-3.5 animate__animated glassmorphism rounded-xl cursor-pointer shadow-card' }>
<div className='dark:text-gray-200 text-center transform hover:scale-150 duration-200 text-xs' title={locale.POST.TOP} >
<FontAwesomeIcon icon={faListOl} />
<div id='toc-drawer-button' className='right-1 fixed bottom-52 z-20'>
<div onClick={props.onClick} className={(show ? 'animate__fadeInRight' : 'hidden') + ' animate__animated animate__faster glassmorphism cursor-pointer' }>
<div className='dark:text-gray-200 text-center transform hover:scale-150 duration-200 text-xs flex justify-center items-center w-8 h-8' title={locale.POST.TOP} >
<FontAwesomeIcon icon={faListOl}/>
</div>
</div>
</div>

View File

@@ -3,10 +3,10 @@ import SideBarDrawer from '@/components/SideBarDrawer'
import { useGlobal } from '@/lib/global'
import { faBars, faSearch } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import { useCallback, useEffect, useRef } from 'react'
import SearchDrawer from './SearchDrawer'
import throttle from 'lodash.throttle'
import { useCallback, useEffect, useRef } from 'react'
import Logo from './Logo'
import SearchDrawer from './SearchDrawer'
let windowTop = 0
@@ -15,7 +15,7 @@ let windowTop = 0
* @param {*} param0
* @returns
*/
const TopNav = ({ tags, currentTag, post, posts, categories, currentCategory, autoHide = true }) => {
const TopNav = ({ tags, currentTag, post, slot, categories, currentCategory, autoHide = true }) => {
const drawer = useRef()
const { locale } = useGlobal()
const searchDrawer = useRef()
@@ -35,38 +35,36 @@ const TopNav = ({ tags, currentTag, post, posts, categories, currentCategory, au
// 监听滚动
useEffect(() => {
if (autoHide) {
if (BLOG.topNavType === 'autoCollapse') {
scrollTrigger()
window.addEventListener('scroll', scrollTrigger)
}
return () => {
autoHide && window.removeEventListener('scroll', scrollTrigger)
BLOG.autoCollapsedNavBar && window.removeEventListener('scroll', scrollTrigger)
}
}, [])
return (<div id='top-nav' className='sticky top-0 z-40 block lg:hidden'>
return (<div id='top-nav' className='z-40 block lg:hidden'>
{/* 侧面抽屉 */}
<SideBarDrawer post={post} currentTag={currentTag} cRef={drawer} tags={tags} posts={posts} categories={categories} currentCategory={currentCategory}/>
<SideBarDrawer post={post} currentTag={currentTag} cRef={drawer} tags={tags} slot={slot} categories={categories} currentCategory={currentCategory}/>
<SearchDrawer cRef={searchDrawer}/>
{/* 导航栏 */}
<div id='sticky-nav' className='flex animate__animated animate__fadeIn fixed lg:relative w-full top-0 z-20 transform duration-500'>
<div className='w-full flex justify-between items-center p-4 glassmorphism'>
<div id='sticky-nav' className={`${BLOG.topNavType !== 'normal' ? 'fixed' : ''} flex animate__animated animate__fadeIn 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={() => { drawer.current.handleSwitchSideDrawerVisible() }}
className='w-8 cursor-pointer dark:text-gray-300'>
className='w-8 cursor-pointer'>
<FontAwesomeIcon icon={faBars} size={'lg'}/>
</div>
</div>
<Link href='/' passHref>
<a>
<h1 className='cursor-pointer ml-2 w-full hover:scale-105 duration-200 transform font-serif dark:text-gray-200 whitespace-nowrap overflow-x-hidden'>{ BLOG.title }</h1>
</a>
</Link>
<div className='flex'>
<Logo/>
</div>
{/* 右侧功能 */}
<div className='mr-1 flex flex-nowrap flex-grow justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<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>

View File

@@ -1,3 +1,4 @@
import BLOG from '@/blog.config'
import CommonHead from '@/components/CommonHead'
import FloatDarkModeButton from '@/components/FloatDarkModeButton'
import Footer from '@/components/Footer'
@@ -35,7 +36,9 @@ const BaseLayout = ({
tags,
meta,
post,
totalPosts,
postCount,
sideBarSlot,
rightAreaSlot,
currentSearch,
currentCategory,
currentTag,
@@ -49,15 +52,15 @@ const BaseLayout = ({
<CommonHead meta={meta} />
<TopNav tags={tags} post={post} posts={totalPosts} currentSearch={currentSearch} categories={categories} currentCategory={currentCategory} />
<TopNav tags={tags} post={post} slot={sideBarSlot} currentSearch={currentSearch} categories={categories} currentCategory={currentCategory} />
<>{headerSlot}</>
<main id='wrapper' className='flex justify-center flex-1 mx-auto md:pt-14 pb-12'>
<aside id='left' className='hidden lg:block flex-col w-60 mr-4'>
<SideAreaLeft targetRef={targetRef} post={post} posts={totalPosts} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
</aside>
<section id='center' className='flex-grow mt-14 md:mt-0 max-w-4xl min-h-screen' ref={targetRef}>
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block'></div>
<main id='wrapper' className='flex justify-center flex-1 mx-auto pb-12'>
<SideAreaLeft targetRef={targetRef} post={post} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
<section id='center' className={`${BLOG.topNavType !== 'normal' ? 'mt-14' : ''} flex-grow md:mt-0 max-w-5xl min-h-screen w-full`} ref={targetRef}>
{onLoading
? <LoadingCover/>
: <>
@@ -65,14 +68,12 @@ const BaseLayout = ({
</>
}
</section>
<aside id='right' className='hidden 2xl:block flex-col w-60 ml-4'>
<SideAreaRight targetRef={targetRef} post={post} posts={totalPosts} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
</aside>
<SideAreaRight targetRef={targetRef} post={post} slot={rightAreaSlot} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
</main>
<Footer title={meta.title}/>
<JumpToTopButton targetRef={targetRef} showPercent={false} />
<JumpToBottomButton targetRef={targetRef} showPercent={false}/>
<JumpToTopButton showPercent={false} />
<JumpToBottomButton showPercent={false}/>
<FloatDarkModeButton/>
</>
)

View File

@@ -45,7 +45,7 @@ bszCaller = {
try {
e(t), scriptTag && scriptTag.parentElement && scriptTag.parentElement.removeChild && scriptTag.parentElement.removeChild(scriptTag)
} catch (t) {
console.log(t), bszTag.hides()
// console.log(t), bszTag.hides()
}
})
}

View File

@@ -1,7 +1,7 @@
import cache from 'memory-cache'
import BLOG from 'blog.config'
const cacheTime = BLOG.isProd ? 60 : 60 * 60
const cacheTime = BLOG.isProd ? 10 * 60 : 120 * 60 // 120 minutes for dev,10 minutes for prod
export async function getCacheFromMemory (key, options) {
return cache.get(key)

View File

@@ -26,7 +26,9 @@ export default {
POSTS: 'Posts',
VISITORS: 'Visitors',
VIEWS: 'Views',
COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!'
COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
RESULT_OF_SEARCH: 'Results Found',
ARTICLE_DETAIL: 'Article Details'
},
PAGINATION: {
PREV: 'Prev',

View File

@@ -21,14 +21,16 @@ export default {
URL_COPIED: '链接已复制!',
TABLE_OF_CONTENTS: '目录',
RELATE_POSTS: '相关文章',
COPYRIGHT: '版权声明',
COPYRIGHT: '声明',
AUTHOR: '作者',
URL: '链接',
ANALYTICS: '统计',
POSTS: '篇文章',
VISITORS: '位访客',
VIEWS: '次查看',
COPYRIGHT_NOTICE: '本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议转载请注明出处'
COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议转载请注明出处。',
RESULT_OF_SEARCH: '篇搜索到的结果',
ARTICLE_DETAIL: '文章详情'
},
PAGINATION: {
PREV: '上一页',

View File

@@ -29,6 +29,7 @@ export async function getAllPosts ({ notionPageData, from, includePage = false }
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const properties = (await getPageProperties(id, pageBlock, schema)) || null
properties.slug = properties.slug ?? properties.id
properties.createdTime = new Date(pageBlock[id].value?.created_time).toString()
properties.lastEditedTime = new Date(pageBlock[id].value?.last_edited_time).toString()
properties.fullWidth = pageBlock[id].value?.format?.page_full_width ?? false

View File

@@ -1,7 +1,52 @@
import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getPostBlocks } from '@/lib/notion/getPostBlocks'
import { idToUuid } from 'notion-utils'
import { getAllCategories } from './getAllCategories'
import { getAllPosts } from './getAllPosts'
import { getAllTags } from './getAllTags'
/**
* 获取博客数据
* @param {*} pageId
* @param {*} from
* @returns
* allPosts 所有博客
* categories 所有分类
* tags 所有标签
*/
export async function getGlobalNotionData ({
pageId = BLOG.notionPageId,
from,
latestPostCount = 5,
tagsCount = 16,
includePage
}) {
const notionPageData = await getNotionPageData({ pageId, from })
const tagOptions = notionPageData.tagOptions
const allPosts = await getAllPosts({ notionPageData, from, includePage })
const postCount = allPosts?.length
const categories = await getAllCategories(allPosts)
const tags = await getAllTags({ allPosts, tagOptions, sliceCount: tagsCount })
// 深拷贝
let latestPosts = Object.create(allPosts)
// 时间排序
latestPosts.sort((a, b) => {
const dateA = new Date(a?.lastEditedTime || a.createdTime)
const dateB = new Date(b?.lastEditedTime || b.createdTime)
return dateB - dateA
})
// 只取前五
latestPosts = latestPosts.slice(0, latestPostCount)
return {
allPosts,
latestPosts,
categories,
postCount,
tags
}
}
/**
* 获取指定notion的collection数据
@@ -9,7 +54,7 @@ import { getPostBlocks } from '@/lib/notion/getPostBlocks'
* @param from 请求来源
* @returns {Promise<JSX.Element|*|*[]>}
*/
export async function getNotionPageData ({ pageId = BLOG.notionPageId, from }) {
export async function getNotionPageData ({ pageId, from }) {
// 尝试从缓存获取
const cacheKey = 'page_record_map_' + pageId
const data = await getDataFromCache(cacheKey)
@@ -54,7 +99,10 @@ async function getPageRecordMapByNotionAPI ({ pageId, from }) {
const tagOptions = getTagOptions(schema)
// Check Type Page-Database和Inline-Database
if (rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view') {
if (
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.warn(`pageId "${pageId}" is not a database`)
return null
}

View File

@@ -20,6 +20,22 @@ export async function getPostBlocks (id, from) {
return null
}
// 去掉不用的字段
for (const j in pageBlock?.block) {
const b = pageBlock?.block[j]
if (b) {
delete b.role
delete b.value?.version
delete b.value?.created_time
delete b.value?.last_edited_time
delete b.value?.created_by_table
delete b.value?.created_by_id
delete b.value?.last_edited_by_table
delete b.value?.last_edited_by_id
delete b.value?.space_id
}
}
if (pageBlock) {
await setDataToCache(cacheKey, pageBlock)
}

View File

@@ -20,8 +20,8 @@ export function generateRss (posts) {
posts.forEach(post => {
feed.addItem({
title: post.title,
id: `${BLOG.link}/${post.slug}`,
link: `${BLOG.link}/${post.slug}`,
id: `${BLOG.link}/article/${post.slug}`,
link: `${BLOG.link}/article/${post.slug}`,
description: post.summary,
date: new Date(post?.date?.start_date || post.createdTime)
})

View File

@@ -33,15 +33,15 @@
"lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0",
"next": "^12.0.5",
"notion-client": "^4.12.0",
"notion-utils": "^4.12.0",
"notion-client": "4.13.0",
"notion-utils": "4.12.0",
"preact": "^10.5.15",
"qrcode.react": "^1.0.1",
"react": "17.0.2",
"react-cookies": "^0.1.1",
"react-cusdis": "^2.0.1",
"react-dom": "17.0.2",
"react-notion-x": "4.6.5",
"react-notion-x": "4.13.0",
"smoothscroll-polyfill": "^0.4.4",
"typed.js": "^2.0.12",
"use-ackee": "^3.0.0"

View File

@@ -16,12 +16,12 @@ export default function Custom404 () {
// 延时3秒如果加载失败就返回首页
setTimeout(() => {
if (window) {
const article = document.getElementById('article-wrapper')
const article = document.getElementById('container')
if (!article) {
router.push('/')
}
}
}, 3000)
}, 30000000)
})
return <BaseLayout meta={{ title: `${BLOG.title} | 页面找不到啦` }}>

View File

@@ -18,7 +18,7 @@ class MyDocument extends Document {
<ThirdPartyScript />
</Head>
<body className='bg-gray-100 dark:bg-gray-900 duration-200'>
<body className={`${BLOG.font} bg-day dark:bg-night duration-200`}>
<Main />
<NextScript />
</body>

View File

@@ -1,8 +1,11 @@
import BLOG from '@/blog.config'
import ArticleDetail from '@/components/ArticleDetail'
import BaseLayout from '@/layouts/BaseLayout'
import { useGlobal } from '@/lib/global'
import { getAllCategories, getAllPosts, getAllTags, getPostBlocks } from '@/lib/notion'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import {
getPostBlocks
} from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import Custom404 from '@/pages/404'
import { getPageTableOfContents } from 'notion-utils'
import 'prismjs'
@@ -12,7 +15,6 @@ import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import React from 'react'
import BLOG from '@/blog.config'
/**
* 关于页面默认取notion中slug为about的文章
@@ -32,15 +34,22 @@ const About = ({ post, blockMap, tags, prev, next, allPosts, categories }) => {
tags: []
}
return <BaseLayout meta={meta} tags={tags} post={post} totalPosts={allPosts} categories={categories}>
<ArticleDetail post={post} blockMap={blockMap} allPosts={allPosts}/>
</BaseLayout>
return (
<BaseLayout
meta={meta}
tags={tags}
post={post}
totalPosts={allPosts}
categories={categories}
>
<ArticleDetail post={post} blockMap={blockMap} allPosts={allPosts} prev={prev} next={next} />
</BaseLayout>
)
}
export async function getStaticProps () {
const from = 'about-props'
const notionPageData = await getNotionPageData({ from })
let allPosts = await getAllPosts({ notionPageData, from, includePage: true })
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true })
const post = allPosts.find(p => p.slug === 'about')
if (!post) {
@@ -50,20 +59,17 @@ export async function getStaticProps () {
const blockMap = await getPostBlocks(post.id, 'slug')
post.toc = []
if (blockMap) {
post.blockMap = blockMap
post.content = Object.keys(blockMap.block)
post.toc = getPageTableOfContents(post, blockMap)
}
allPosts = allPosts.filter(post => post.type[0] === 'Post')
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const categories = await getAllCategories(allPosts)
const index = allPosts.indexOf(post)
const prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
const next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
return {
props: { post, blockMap, tags, prev, next, allPosts, categories },
props: { post, blockMap, tags, prev, next, allPosts, categories, postCount, latestPosts },
revalidate: 1
}
}

View File

@@ -1,33 +1,31 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BaseLayout from '@/layouts/BaseLayout'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import React, { useEffect } from 'react'
import { useGlobal } from '@/lib/global'
import BlogPostArchive from '@/components/BlogPostArchive'
import Live2D from '@/components/Live2D'
import BaseLayout from '@/layouts/BaseLayout'
import { useGlobal } from '@/lib/global'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React, { useEffect } from 'react'
export async function getStaticProps () {
const from = 'index'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const { allPosts, categories, tags, postCount } =
await getGlobalNotionData({ from, includePage: true })
return {
props: {
allPosts,
posts: allPosts,
tags,
categories
categories,
postCount
},
revalidate: 1
}
}
const Index = ({ allPosts, tags, categories }) => {
const Index = ({ posts, tags, categories, postCount }) => {
const { locale } = useGlobal()
// 深拷贝
const postsSortByDate = Object.create(allPosts)
const postsSortByDate = Object.create(posts)
// 时间排序
postsSortByDate.sort((a, b) => {
@@ -42,7 +40,7 @@ const Index = ({ allPosts, tags, categories }) => {
type: 'website'
}
const archivePosts = { }
const archivePosts = {}
postsSortByDate.forEach(post => {
const date = post.date.start_date.slice(0, 7)
@@ -53,29 +51,32 @@ const Index = ({ allPosts, tags, categories }) => {
}
})
useEffect(
() => {
if (window) {
const anchor = window.location.hash
if (anchor) {
setTimeout(() => {
const anchorElement = document.getElementById(anchor.substring(1))
if (anchorElement) { anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' }) }
}, 300)
}
useEffect(() => {
if (window) {
const anchor = window.location.hash
if (anchor) {
setTimeout(() => {
const anchorElement = document.getElementById(anchor.substring(1))
if (anchorElement) {
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
}, 300)
}
},
[]
)
}
}, [])
return (
<BaseLayout meta={meta} tags={tags} categories={categories}>
<div className='mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 rounded-xl shadow-md min-h-full'>
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive key={archiveTitle} posts={archivePosts[archiveTitle]} archiveTitle={archiveTitle}/>
))}
</div>
{BLOG.showPet && <Live2D/>}
<BaseLayout meta={meta} tags={tags} categories={categories} postCount={postCount}>
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full">
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
<Live2D />
</BaseLayout>
)
}

View File

@@ -1,19 +1,27 @@
import { getAllCategories, getAllPosts, getAllTags, getPostBlocks } from '@/lib/notion'
import BLOG from '@/blog.config'
import { getPageTableOfContents } from 'notion-utils'
import BaseLayout from '@/layouts/BaseLayout'
import Custom404 from '@/pages/404'
import ArticleDetail from '@/components/ArticleDetail'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import BaseLayout from '@/layouts/BaseLayout'
import { getAllPosts, getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import Custom404 from '@/pages/404'
import { getPageTableOfContents } from 'notion-utils'
/**
* 根据notion的slug访问页面
* @param {*} param0
* @returns
*/
const Slug = ({ post, blockMap, tags, prev, next, allPosts, recommendPosts, categories }) => {
const Slug = ({
post,
tags,
prev,
next,
allPosts,
recommendPosts,
categories,
postCount,
latestPosts
}) => {
if (!post) {
return <Custom404 />
}
@@ -24,26 +32,42 @@ const Slug = ({ post, blockMap, tags, prev, next, allPosts, recommendPosts, cate
tags: post.tags
}
return <BaseLayout meta={meta} tags={tags} post={post} totalPosts={allPosts} categories={categories}>
<ArticleDetail post={post} blockMap={blockMap} recommendPosts={recommendPosts} prev={prev} next={next}/>
</BaseLayout>
return (
<BaseLayout
meta={meta}
tags={tags}
post={post}
postCount={postCount}
latestPosts={latestPosts}
totalPosts={allPosts}
categories={categories}
>
<ArticleDetail
post={post}
recommendPosts={recommendPosts}
prev={prev}
next={next}
/>
</BaseLayout>
)
}
export async function getStaticPaths () {
let posts = []
if (BLOG.isProd) {
posts = await getAllPosts({ from: 'slug - paths', includePage: true })
posts = await getAllPosts({ from: 'slug - paths', includePage: false })
}
return {
paths: posts.map(row => `${BLOG.path}/article/${row.slug}`),
paths: posts.map(row => ({ params: { slug: row.slug } })),
fallback: true
}
}
export async function getStaticProps ({ params: { slug } }) {
const from = `slug-props-${slug}`
const notionPageData = await getNotionPageData({ from })
let allPosts = await getAllPosts({ notionPageData, from, includePage: true })
const { allPosts, categories, tags, postCount, latestPosts } =
await getGlobalNotionData({ from, includePage: false })
const post = allPosts.find(p => p.slug === slug)
if (!post) {
@@ -52,14 +76,12 @@ export async function getStaticProps ({ params: { slug } }) {
const blockMap = await getPostBlocks(post.id, 'slug')
if (blockMap) {
post.blockMap = blockMap
post.content = Object.keys(blockMap.block)
post.toc = getPageTableOfContents(post, blockMap)
delete post.content
}
allPosts = allPosts.filter(post => post.type[0] === 'Post')
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const categories = await getAllCategories(allPosts)
// 上一篇、下一篇文章关联
const index = allPosts.indexOf(post)
const prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
@@ -68,7 +90,17 @@ export async function getStaticProps ({ params: { slug } }) {
const recommendPosts = getRecommendPost(post, allPosts)
return {
props: { post, blockMap, tags, prev, next, allPosts, recommendPosts, categories },
props: {
post,
tags,
prev,
next,
allPosts,
recommendPosts,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
@@ -86,11 +118,7 @@ function getRecommendPost (post, allPosts, count = 5) {
if (post.tags && post.tags.length) {
const currentTag = post.tags[0]
filteredPosts = filteredPosts.filter(
p =>
p &&
p.tags &&
p.tags.includes(currentTag) &&
p.slug !== post.slug
p => p && p.tags && p.tags.includes(currentTag) && p.slug !== post.slug
)
}
shuffleSort(filteredPosts)

View File

@@ -1,26 +1,25 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BlogPostListScroll from '@/components/BlogPostListScroll'
import CategoryList from '@/components/CategoryList'
import StickyBar from '@/components/StickyBar'
import BaseLayout from '@/layouts/BaseLayout'
import BlogPostListScroll from '@/components/BlogPostListScroll'
import React from 'react'
import CategoryList from '@/components/CategoryList'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import { useGlobal } from '@/lib/global'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React from 'react'
export default function Category ({ tags, allPosts, filteredPosts, category, categories }) {
export default function Category ({ tags, posts, category, categories, latestPosts, postCount }) {
const { locale } = useGlobal()
const meta = {
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <BaseLayout meta={meta} tags={tags} currentCategory={category} totalPosts={allPosts} categories={categories}>
return <BaseLayout meta={meta} tags={tags} currentCategory={category} postCount={postCount} latestPosts={latestPosts} categories={categories}>
<StickyBar>
<CategoryList currentCategory={category} categories={categories} />
</StickyBar>
<div className='md:mt-8'>
<BlogPostListScroll posts={filteredPosts} tags={tags} currentCategory={category}/>
<BlogPostListScroll posts={posts} tags={tags} currentCategory={category}/>
</div>
</BaseLayout>
}
@@ -28,31 +27,26 @@ export default function Category ({ tags, allPosts, filteredPosts, category, cat
export async function getStaticProps ({ params }) {
const from = 'category-props'
const category = params.category
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from })
const filteredPosts = allPosts.filter(
post => post && post.category && post.category.includes(category)
)
return {
props: {
tags,
allPosts,
filteredPosts,
posts: filteredPosts,
category,
categories
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
export async function getStaticPaths () {
let posts = []
let categories = []
posts = await getAllPosts({ from: 'category-path' })
categories = await getAllCategories(posts)
const from = 'category-paths'
const { categories } = await getGlobalNotionData({ from })
return {
paths: Object.keys(categories).map(category => ({ params: { category } })),
fallback: true

View File

@@ -1,21 +1,20 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BaseLayout from '@/layouts/BaseLayout'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import { useGlobal } from '@/lib/global'
import React from 'react'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { faFolder, faThList } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link'
import React from 'react'
export default function Category ({ tags, allPosts, categories }) {
export default function Category ({ tags, allPosts, categories, postCount, latestPosts }) {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <BaseLayout meta={meta} totalPosts={allPosts} tags={tags}>
return <BaseLayout meta={meta} totalPosts={allPosts} tags={tags} postCount={postCount} latestPosts={latestPosts}>
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faThList} className='mr-4' />{locale.COMMON.CATEGORY}:</div>
<div id='category-list' className='duration-200 flex flex-wrap'>
@@ -31,17 +30,16 @@ export default function Category ({ tags, allPosts, categories }) {
}
export async function getStaticProps () {
const from = 'tag-index-props'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, sliceCount: 12, tagOptions })
const from = 'category-index-props'
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from })
return {
props: {
tags,
allPosts,
categories
categories,
postCount,
latestPosts
},
revalidate: 1
}

View File

@@ -1,25 +1,46 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BaseLayout from '@/layouts/BaseLayout'
import BlogPostListPage from '@/components/BlogPostListPage'
import BlogPostListScroll from '@/components/BlogPostListScroll'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import Header from '@/components/Header'
import LatestPostsGroup from '@/components/LatestPostsGroup'
import BaseLayout from '@/layouts/BaseLayout'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
export async function getStaticProps () {
const from = 'index'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const { allPosts, latestPosts, categories, tags, postCount } = await getGlobalNotionData({ from })
const meta = {
title: `${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
// 处理分页
const page = 1
let postsToShow = []
if (BLOG.postListStyle !== 'page') {
postsToShow = Array.from(allPosts)
} else {
postsToShow = allPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
)
for (const i in postsToShow) {
const post = postsToShow[i]
const blockMap = await getPostBlocks(post.id, 'slug')
if (blockMap) {
post.blockMap = blockMap
}
}
console.log('加载文章预览完成')
}
return {
props: {
allPosts,
posts: postsToShow,
latestPosts,
postCount,
tags,
categories,
meta
@@ -28,16 +49,26 @@ export async function getStaticProps () {
}
}
const Index = ({ allPosts, tags, meta, categories }) => {
const Index = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
return (
<BaseLayout
headerSlot={<Header />}
headerSlot={BLOG.home.showHomeBanner && <Header />}
meta={meta}
tags={tags}
totalPosts={allPosts}
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
rightAreaSlot={
BLOG.widget?.showLatestPost && <LatestPostsGroup posts={latestPosts} />
}
postCount={postCount}
categories={categories}
>
<BlogPostListScroll posts={allPosts} tags={tags} />
{BLOG.postListStyle !== 'page'
? (
<BlogPostListScroll posts={posts} tags={tags} showSummary={true} />
)
: (
<BlogPostListPage posts={posts} tags={tags} postCount={postCount} />
)}
</BaseLayout>
)
}

78
pages/page/[page].js Normal file
View File

@@ -0,0 +1,78 @@
import BLOG from '@/blog.config'
import BlogPostListPage from '@/components/BlogPostListPage'
import Header from '@/components/Header'
import LatestPostsGroup from '@/components/LatestPostsGroup'
import BaseLayout from '@/layouts/BaseLayout'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import Custom404 from '../404'
const Page = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
if (!meta || BLOG.postListStyle !== 'page') {
return <Custom404/>
}
return (
<BaseLayout
headerSlot={BLOG.home.showHomeBanner && <Header />}
meta={meta}
tags={tags}
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
rightAreaSlot={BLOG.widget?.showLatestPost && <LatestPostsGroup posts={latestPosts} />}
postCount={postCount}
categories={categories}
>
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
</BaseLayout>
)
}
export async function getStaticPaths () {
const from = 'page-paths'
const { postCount } = await getGlobalNotionData({ from })
const totalPages = Math.ceil(postCount / BLOG.postsPerPage)
return {
// remove first page, we 're not gonna handle that.
paths: Array.from({ length: totalPages - 1 }, (_, i) => ({ params: { page: '' + (i + 2) } })),
fallback: true
}
}
export async function getStaticProps ({ params: { page } }) {
const from = `page-${page}`
const { allPosts, latestPosts, categories, tags, postCount } = await getGlobalNotionData({ from })
const meta = {
title: `${page} | Page | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
// 处理分页
const postsToShow = allPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
)
for (const i in postsToShow) {
const post = postsToShow[i]
const blockMap = await getPostBlocks(post.id, 'slug')
if (blockMap) {
post.blockMap = blockMap
}
}
console.log('加载文章预览完成')
return {
props: {
page,
posts: postsToShow,
postCount,
latestPosts,
tags,
categories,
meta
},
revalidate: 1
}
}
export default Page

View File

@@ -1,57 +1,66 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BaseLayout from '@/layouts/BaseLayout'
import StickyBar from '@/components/StickyBar'
import BlogPostListScroll from '@/components/BlogPostListScroll'
import { useRouter } from 'next/router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import StickyBar from '@/components/StickyBar'
import BaseLayout from '@/layouts/BaseLayout'
import { useGlobal } from '@/lib/global'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useRouter } from 'next/router'
export async function getStaticProps () {
const from = 'search-props'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions })
const { allPosts, categories, tags, postCount, latestPosts } =
await getGlobalNotionData({ from })
return {
props: {
allPosts,
posts: allPosts,
tags,
categories
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
const Search = ({ allPosts, tags, categories }) => {
// 处理查询过滤 支持标签、关键词过滤
const Search = ({ posts, tags, categories, postCount, latestPosts }) => {
let filteredPosts = []
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = allPosts.filter(post => {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = posts
}
const { locale } = useGlobal()
const meta = {
title: `${locale.NAV.SEARCH} ${searchKey}| ${BLOG.title} `,
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.title} `,
description: BLOG.description,
type: 'website'
}
return (
<BaseLayout meta={meta} tags={tags} totalPosts={allPosts} currentSearch={searchKey} categories={categories}>
<StickyBar>
<div className='p-4 dark:text-gray-200'><FontAwesomeIcon icon={faSearch} className='mr-1'/> {locale.NAV.SEARCH} {searchKey}</div>
</StickyBar>
<div className='md:mt-5'>
<BlogPostListScroll posts={filteredPosts} tags={tags} currentSearch={searchKey} />
<BaseLayout
meta={meta}
tags={tags}
postCount={postCount}
currentSearch={searchKey}
categories={categories}
>
<StickyBar>
<div className="p-4 dark:text-gray-200">
<FontAwesomeIcon icon={faSearch} className="mr-1" />{' '}
{filteredPosts.length} {locale.COMMON.RESULT_OF_SEARCH}
</div>
</StickyBar>
<div className="md:mt-5">
<BlogPostListScroll posts={filteredPosts} tags={tags} showSummary={true}/>
</div>
</BaseLayout>
)
}

View File

@@ -1,13 +1,13 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import StickyBar from '@/components/StickyBar'
import BaseLayout from '@/layouts/BaseLayout'
import BlogPostListScroll from '@/components/BlogPostListScroll'
import StickyBar from '@/components/StickyBar'
import TagList from '@/components/TagList'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import BaseLayout from '@/layouts/BaseLayout'
import { useGlobal } from '@/lib/global'
import { getAllPosts } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
export default function Tag ({ tags, allPosts, filteredPosts, tag, categories }) {
export default function Tag ({ tags, posts, tag, categories, postCount, latestPosts }) {
const { locale } = useGlobal()
const meta = {
@@ -21,12 +21,12 @@ export default function Tag ({ tags, allPosts, filteredPosts, tag, categories })
const currentTag = tags?.find(r => r?.name === tag)
const newTags = currentTag ? [currentTag].concat(tags.filter(r => r?.name !== tag)) : tags.filter(r => r?.name !== tag)
return <BaseLayout meta={meta} tags={tags} currentTag={tag} categories={categories} totalPosts={allPosts}>
return <BaseLayout meta={meta} tags={tags} currentTag={tag} categories={categories} postCount={postCount} latestPosts={latestPosts}>
<StickyBar>
<TagList tags={newTags} currentTag={tag}/>
</StickyBar>
<div className='md:mt-8'>
<BlogPostListScroll posts={filteredPosts} tags={tags} currentTag={tag}/>
<BlogPostListScroll posts={posts} tags={tags} currentTag={tag}/>
</div>
</BaseLayout>
}
@@ -34,21 +34,18 @@ export default function Tag ({ tags, allPosts, filteredPosts, tag, categories })
export async function getStaticProps ({ params }) {
const tag = params.tag
const from = 'tag-props'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, tagOptions, sliceCount: 0 })
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
const filteredPosts = allPosts.filter(
post => post && post.tags && post.tags.includes(tag)
)
return {
props: {
tags,
allPosts,
filteredPosts,
posts: filteredPosts,
tag,
categories
categories,
postCount,
latestPosts
},
revalidate: 1
}

View File

@@ -1,21 +1,20 @@
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import BaseLayout from '@/layouts/BaseLayout'
import TagItem from '@/components/TagItem'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import BaseLayout from '@/layouts/BaseLayout'
import { useGlobal } from '@/lib/global'
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { faTags } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
export default function Tag ({ tags, allPosts, categories }) {
export default function Tag ({ tags, allPosts, categories, postCount, latestPosts }) {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <BaseLayout meta={meta} categories={categories} totalPosts={allPosts}>
return <BaseLayout meta={meta} categories={categories} totalPosts={allPosts} postCount={postCount} latestPosts={latestPosts}>
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div className='dark:text-gray-200 mb-5'><FontAwesomeIcon icon={faTags} className='mr-4'/>{locale.COMMON.TAGS}:</div>
<div id='tags-list' className='duration-200 flex flex-wrap'>
@@ -29,16 +28,15 @@ export default function Tag ({ tags, allPosts, categories }) {
export async function getStaticProps () {
const from = 'tag-index-props'
const notionPageData = await getNotionPageData({ from })
const allPosts = await getAllPosts({ notionPageData, from })
const categories = await getAllCategories(allPosts)
const tagOptions = notionPageData.tagOptions
const tags = await getAllTags({ allPosts, sliceCount: 0, tagOptions })
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from, includePage: true, tagsCount: 0 })
return {
props: {
tags,
allPosts,
categories
posts: allPosts,
categories,
postCount,
latestPosts
},
revalidate: 1
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -159,4 +159,8 @@ nav {
background: rgba(31, 41, 55, .75);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.medium-zoom-overlay{
background: none !important;
}

View File

@@ -72,6 +72,7 @@
line-height: 1.5;
color: var(--fg-color);
caret-color: var(--fg-color);
font-family: inherit;
}
.notion > * {
@@ -386,8 +387,9 @@
}
.notion-h1 {
font-size: 1.875em;
font-size: 1.575em;
margin-top: 1.08em;
@apply border-b w-full
}
.notion-header-anchor {
@@ -649,7 +651,7 @@ svg.notion-page-icon {
}
.notion-list li {
padding: 6px 0;
padding: 1px 0;
white-space: pre-wrap;
}
@@ -1583,7 +1585,7 @@ svg.notion-page-icon {
/* NOTION CSS OVERRIDE */
.notion {
@apply text-gray-600 dark:text-gray-300;
@apply dark:text-gray-300;
overflow-wrap: break-word;
}
.notion,

View File

@@ -1,9 +1,28 @@
const BLOG = require('./blog.config')
const { fontFamily } = require('tailwindcss/defaultTheme')
const CJK = require('./lib/cjk')
const fontSansCJK = !CJK()
? []
: [`"Noto Sans CJK ${CJK()}"`, `"Noto Sans ${CJK()}"`]
const fontSerifCJK = !CJK()
? []
: [`"Noto Serif CJK ${CJK()}"`, `"Noto Serif ${CJK()}"`]
module.exports = {
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js'],
darkMode: BLOG.appearance === 'class' ? 'media' : 'class', // or 'media' or 'class'
theme: {
fontFamily: {
sans: ['"IBM Plex Sans"', ...fontFamily.sans, ...fontSansCJK],
serif: ['"Source Serif"', ...fontFamily.serif, ...fontSerifCJK],
noEmoji: [
'"IBM Plex Sans"',
'ui-sans-serif',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'sans-serif'
]
},
extend: {
colors: {
day: {