Merge branch 'main' into theme-Fukasawa

# Conflicts:
#	blog.config.js
This commit is contained in:
tangly1024
2022-01-15 22:51:15 +08:00
152 changed files with 4746 additions and 2581 deletions

9
.gitignore vendored
View File

@@ -33,10 +33,13 @@ yarn-error.log*
# vercel
.vercel
# sitemap
/public/robots.txt
/public/sitemap.xml
# dev
/data.json
/yarn.lock
.idea
.vscode
# sitemap
/public/robots.txt
/public/sitemap.xml

View File

@@ -1,6 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.625" y="0.625" width="48.75" height="48.75" rx="14.375" fill="black" stroke="white" stroke-width="1.25"/>
<path d="M18.0876 23.1894L24.7284 18.7483L31.7252 23.2522C32.5632 23.2522 33.4011 23.0009 34.1134 22.5609L39.9791 18.7902L24.1208 11.5L9.75 19.6281L16.3489 22.7914C16.8935 23.0428 17.501 23.1685 18.0876 23.1894V23.1894ZM26.4881 36.1776L12.222 29.3483L9.75 30.7519L26.3624 38.6915L40 29.914L37.7794 28.9084L26.4881 36.1776Z" fill="white"/>
<path d="M26.4881 32.2602L12.222 25.4519L9.75 26.8345L26.3624 34.7741L40 26.0175L37.7794 24.991L26.4881 32.2602Z" fill="white"/>
<path d="M34.8257 23.5874C33.9249 24.174 32.8565 24.4882 31.7881 24.4882H31.7671L18.2552 24.4254C17.4382 24.4254 16.6002 24.2368 15.8461 23.8807L11.6354 21.8696L9.75 22.938L26.3624 30.8567L40 22.1001L38.345 21.3459L34.8257 23.5874V23.5874Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 944 B

View File

@@ -17,11 +17,7 @@
</a>
</p>
演示地址:[https://www.tangly1024.com/](https://www.tangly1024.com/)
<details><summary>截图</summary>
<img src='https://github.com/tangly1024/NotionNext/blob/main/screenshot.png?raw=true'/>
</details>
演示地址:[https://tangly1024.com/](https://tangly1024.com/)
## 亮点 ✨
@@ -37,8 +33,10 @@
**🚙 &nbsp;全功能,完全不操心**
- 评论、宽页面、搜索标签
- 订阅、网站统计、Web Vital 分析…… 还有更多功能待你发现
- 评论、搜索标签、分类
- 订阅、网站统计
- 本地化多语言
- 服务端渲染、优秀的SEO
**🎨 &nbsp;美观,轻松自定义**
@@ -46,30 +44,54 @@
- 使用 Tailwind CSS轻松实现二次开发
**🕸 &nbsp;网址美观、搜索引擎优化**
- 更多特性、欢迎移步[我的博客](https://tangly1024.com/article/notion-next)查看
## 更新日志
请移步 [更新文档](https://docs.tangly1024.com/zh/changelog)查看
## 快速起步
- 给这个项目点个小星星 😉
- 将 [这个 Notion 模板](https://www.notion.so/68be9021bca34b8e89f0246f27e608df) 制作副本,并分享这个页面给所有人
- 将 [这个 Notion 模板](https://tanghh.notion.site/02ab3b8678004aa69e9e415905ef32a5) 制作副本,并分享这个页面给所有人
- [Fork](https://github.com/tangly1024/NotionNext/fork) 这个项目
- `blog.config.js` 配置相关选项
- _(可选)_ 用自己的图片替换 `/public` 文件夹里的 `avatar.svg``favicon.svg``favicon.ico`
- _(可选)_ 用自己的图片替换 `/public` 文件夹里的 `avatar.jpg``favicon.svg``favicon.ico`
- 在 [Vercel](https://vercel.com) 上部署这个项目, 设定一下环境变量:
- `NOTION_PAGE_ID`: 你刚刚分享出去的 Notion 页面网址中的页面 ID通常是网址中工作区地址后的 32 位字符串
- `NOTION_ACCESS_TOKEN`(可选): 如果你决定不分享你的数据库,你可以使用 token 来让网页从 Notion 数据库中抓取数据。你可以在你的浏览器 cookies 中找到它,名称是 `token_v2'。
- Notion token 的有效期只有 180 天,请确保在 Vercel Dashboard 上手动更新,我们可能会在未来切换到官方 API 来解决这个问题。此外如果数据库是非公开到Notion 中的图片可能无法正常显示到网页上。
- `blog.config.js` 配置相关选项,更多关于配置的说明,请移步[NotionNext文档](https://docs.tangly1024.com/zh)
- **稍微等等就可以访问了!** 简单吗?
## 技术细节
- **生成**: Next.js SSG 和 Incremental Static Regeneration
- **页面渲染**: [React-notion-x](https://github.com/NotionX/react-notion-x)
- **样式**: Tailwind CSS 和 `@tailwindcss/jit` compiler
- **评论**: Gitalk,Cusdis,Utterances
## 快速开发
```bash
yarn # 安装依赖
yarn run dev # 本地开发
yarn run build # 本地打包编译
yarn run start # 本地启动NextJS服务
```
## 引用技术
- **框架**: Next.js
- **渲染**: [React-notion-x](https://github.com/NotionX/react-notion-x)
- **样式**: [Tailwind CSS](https://www.tailwindcss.cn/) 和 `@tailwindcss/jit` compiler
- **评论**: Gitalk, Cusdis, Utterances
- **图标**[fontawesome](https://fontawesome.com/v5.15/icons?d=gallery)
## 页面样式主题
- 仿照 [fukasawa](https://andersnoren.se/themes/fukasawa)
- 仿照 [fukasawa](https://andersnoren.se/themes/fukasawa) 分支-https://github.com/tangly1024/NotionNext/tree/theme-Fukasawa
<details><summary>fukasawa截图</summary>
<img src='https://github.com/tangly1024/NotionNext/blob/main/docs/screenshot-fukasawa.png?raw=true'/>
</details>
- 仿照 [youtube] 主题 分支-https://github.com/tangly1024/NotionNext/tree/themw-youtube
<details><summary>youtube截图</summary>
<img src='https://github.com/tangly1024/NotionNext/blob/main/docs/screenshot-youtube.png?raw=true'/>
</details>
## License

View File

@@ -1,50 +1,68 @@
const BLOG = {
title: '塘里博客',
author: 'tangly1024',
email: 'tlyong1992@hotmail.com',
link: 'https://tangly1024.com',
description: '唐风集里,收卷波澜',
lang: 'zh-CN', // ['zh-CN','en-US']
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, // 列表展示文章预览
previewLines: 12, // 预览文章的篇幅
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', // ['font-sans', 'font-serif', 'font-mono']
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.
postsPerPage: 9,
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: 'https://weibo.com/u/2245301913',
seo: {
keywords: ['Blog', 'Website', 'Notion'],
googleSiteVerification: '' // Remove the value or replace it with your own google site verification code
topNavType: 'normal', // ['fixed','autoCollapse','normal'] 分别是固定顶部、固定底部滑动时自动折叠,不固定
menu: { // 菜单栏设置
showAbout: false, // 显示关于
showCategory: true, // 显示分类
showTag: true, // 显示标签
showArchive: true, // 显示归档
showSearch: true // 显示搜索
},
notionPageId: process.env.NOTION_PAGE_ID || 'bee1fccfa3bd47a1a7be83cc71372d83', // DO NOT CHANGE THIS
notionAccessToken: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
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-5EV4HZD0XX' // e.g: G-XXXXXXXXXX
},
baidyAnalytics: 'f683ef76f06bb187cbed5546f6f28f28', // e.g only need xxxxx -> https://hm.baidu.com/hm.js?xxxxx
busuanzi: true // see http://busuanzi.ibruce.info/
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: false, // 显示回底
showDarkMode: false, // 显示日间/夜间模式切换
showToc: true, // 移动端显示悬浮目录
showShareBar: false, // 文章分享功能
showRelatePosts: true, // 相关文章推荐
showCopyRight: true, // 文章版权声明
showLatestPost: false, // 右侧边栏显示最近更新
showCategoryList: false, // 右侧边栏显示文章分类列表
showTagList: false // 右侧边栏显示标签分类列表
},
comment: {
// support provider: gitalk, utterances, cusdis
provider: 'cusdis', // leave it empty if you don't need any comment plugin
socialLink: { // 社交链接,如不需要展示可以留空白,例如 weibo:''
weibo: 'https://weibo.com/tangly1024',
twitter: 'https://twitter.com/troy1024_1',
github: 'https://github.com/tangly1024',
telegram: 'https://t.me/tangly_1024'
},
comment: { // 评论插件,支持 gitalk, utterances, cusdis
provider: 'gitalk', // 不需要则留空白
gitalkConfig: {
repo: 'NotionNext', // The repository of store comments
owner: 'tangly1024',
admin: ['tangly1024'],
clientID: 'be7864a16b693e256f8f',
clientSecret: 'dbd0f6d9ceea8940f6ed20936b415274b8fe66a2',
clientID: process.env.GITALK_ID || 'be7864a16b693e256f8f',
clientSecret: process.env.GITALK_SECRET || 'dbd0f6d9ceea8940f6ed20936b415274b8fe66a2',
distractionFreeMode: false
},
cusdisConfig: {
@@ -54,11 +72,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'
}
},
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
googleAdsenseId: 'ca-pub-2708419466378217',
DaoVoiceId: '' // DaoVoice http://dashboard.daovoice.io/get-started
seo: {
googleSiteVerification: '' // Remove the value or replace it with your own google site verification code
},
googleAdsenseId: 'ca-pub-2708419466378217', // 谷歌广告ID
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,31 +0,0 @@
import BLOG from '@/blog.config'
import TagItem from '@/components/TagItem'
const BlogPost = ({ post }) => {
return (
<article key={post.id}
className='inline-block border dark:border-gray-600 my-2 shadow-card w-full md:max-w-md bg-white dark:bg-gray-700 dark:hover:bg-gray-600 overflow-hidden'>
{/* 封面图 */}
{post.page_cover && post.page_cover.length > 1 && (
<a href={`${BLOG.path}/article/${post.slug}`} className='md:flex-shrink-0 md:w-52 md:h-52 rounded-lg'>
<img className='w-full max-h-60 object-cover cursor-pointer transform hover:scale-110 duration-500' src={post.page_cover} alt={post.title} />
</a>
)}
<div className='px-8 py-6'>
<a href={`${BLOG.path}/article/${post.slug}`}
className='block my-3 text-2xl leading-tight font-semibold text-black dark:text-gray-100 hover:underline'>
{post.title}
</a>
<p className='mt-2 text-gray-500 dark:text-gray-300 text-sm'>{post.summary}</p>
<div className='flex flex-nowrap leading-8 py-2'>
{post.tags.map(tag => (
<TagItem key={tag} tag={tag} />
))}
</div>
</div>
</article>
)
}
export default BlogPost

View File

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

17
components/Busuanzi.js Normal file
View File

@@ -0,0 +1,17 @@
import busuanzi from '@/lib/busuanzi'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function Busuanzi () {
const router = useRouter()
useEffect(() => {
const busuanziRouteChange = url => {
busuanzi.fetch()
}
router.events.on('routeChangeComplete', busuanziRouteChange)
return () => {
router.events.off('routeChangeComplete', busuanziRouteChange)
}
}, [router.events])
return null
}

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useTheme } from '@/lib/theme'
import { useEffect, useState } from 'react'
import { useGlobal } from '@/lib/global'
import 'gitalk/dist/gitalk.css'
const GitalkComponent = dynamic(
() => {
@@ -25,46 +25,48 @@ const CusdisComponent = dynamic(
const Comment = ({ frontMatter }) => {
const router = useRouter()
const { theme } = useTheme()
const { theme } = useGlobal()
return <div className='comment text-gray-800 dark:text-gray-300'>
<div className='font-bold pt-2 pb-4 '>留下评论</div>
{/* 评论插件 */}
{BLOG.comment.provider === 'gitalk' && (
<GitalkComponent
options={{
id: frontMatter.id,
title: frontMatter.title,
clientID: BLOG.comment.gitalkConfig.clientID,
clientSecret: BLOG.comment.gitalkConfig.clientSecret,
repo: BLOG.comment.gitalkConfig.repo,
owner: BLOG.comment.gitalkConfig.owner,
admin: BLOG.comment.gitalkConfig.admin,
distractionFreeMode: BLOG.comment.gitalkConfig.distractionFreeMode
}}
/>
)}
{BLOG.comment.provider === 'utterances' && (
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
)}
{BLOG.comment.provider === 'cusdis' && (
<>
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
<CusdisComponent
attrs={{
host: BLOG.comment.cusdisConfig.host,
appId: BLOG.comment.cusdisConfig.appId,
pageId: frontMatter.id,
pageTitle: frontMatter.title,
pageUrl: BLOG.link + router.asPath,
theme: theme
}}
lang={BLOG.lang.toLowerCase()}
/>
</>
)}</div>
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>
)
)
}
export default Comment

View File

@@ -2,12 +2,19 @@ import BLOG from '@/blog.config'
import Head from 'next/head'
const CommonHead = ({ meta }) => {
const url = BLOG.path.length ? `${BLOG.link}/${BLOG.path}` : BLOG.link
let url = BLOG.path.length ? `${BLOG.link}/${BLOG.path}` : BLOG.link
if (meta) {
url = `${url}/${meta.slug}`
}
const title = meta?.title || BLOG.title
const description = meta?.description || BLOG.description
const type = meta?.type || 'website'
const keywords = meta?.tags || BLOG.keywords
return <Head>
<title>{meta.title}</title>
<meta content={BLOG.darkBackground} name='theme-color' />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>{title}</title>
<meta name='theme-color' content={BLOG.darkBackground} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name='robots' content='follow, index' />
<meta charSet='UTF-8' />
{BLOG.seo.googleSiteVerification && (
@@ -16,21 +23,19 @@ const CommonHead = ({ meta }) => {
content={BLOG.seo.googleSiteVerification}
/>
)}
{BLOG.seo.keywords && (
<meta name='keywords' content={BLOG.seo.keywords.join(', ')} />
{keywords && (
<meta name='keywords' content={keywords.join(', ')} />
)}
<meta name='description' content={meta.description} />
<meta name='description' content={description} />
<meta property='og:locale' content={BLOG.lang} />
<meta property='og:title' content={meta.title} />
<meta property='og:description' content={meta.description} />
<meta
property='og:url'
content={meta.slug ? `${url}/${meta.slug}` : url}
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:url' content={url}
/>
<meta property='og:type' content={meta.type} />
<meta property='og:type' content={type} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={meta.description} />
<meta name='twitter:title' content={meta.title} />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
{meta.type === 'article' && (
<>
<meta

View File

@@ -1,8 +1,13 @@
import BLOG from '@/blog.config'
const ThirdPartyScript = () => {
/**
* 第三方代码 统计脚本
* @returns {JSX.Element}
* @constructor
*/
const CommonScript = () => {
return (<>
{BLOG.DaoVoiceId && (<>
{BLOG.comment?.DaoVoiceId && (<>
{/* DaoVoice 反馈 */}
<script async dangerouslySetInnerHTML={{
__html: `
@@ -13,20 +18,39 @@ const ThirdPartyScript = () => {
<script async dangerouslySetInnerHTML={{
__html: `
daovoice('init', {
app_id: "${BLOG.DaoVoiceId}"
app_id: "${BLOG.comment.DaoVoiceId}"
});
daovoice('update');
`
}}
/>
</>)}
{/* GoogleAdsense 广告植入 */}
{BLOG.googleAdsenseId && (<script data-ad-client={BLOG.googleAdsenseId} async
src='https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'/>)}
{BLOG.comment?.TidioId && (<>
{/* Tidio在线反馈 */}
<script async
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 && (<>
{/* GoogleAdsense */}
{BLOG.googleAdsenseId && (
<script data-ad-client={BLOG.googleAdsenseId} async
src='https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js' />
)}
{/* ackee统计脚本 */}
{BLOG.analytics.provider === 'ackee' && (
@@ -36,14 +60,14 @@ const ThirdPartyScript = () => {
/>
)}
{/* 百度统计 */}
{BLOG.analytics.baidyAnalytics && (
{BLOG.analytics.baiduAnalytics && (
<script async
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${BLOG.analytics.baidyAnalytics}";
hm.src = "https://hm.baidu.com/hm.js?${BLOG.analytics.baiduAnalytics}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
@@ -51,19 +75,13 @@ const ThirdPartyScript = () => {
}}
/>
)}
{/* 不蒜子 */}
{BLOG.analytics.busuanzi && (
<script async
src={'//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'}
/>
)}
{/* 站长统计 */}
{BLOG.isProd && (
{BLOG.analytics.cnzzAnalytics && (
<script async
dangerouslySetInnerHTML={{
__html: `
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_1279970751'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D1279970751' type='text/javascript'%3E%3C/script%3E"));
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${BLOG.analytics.cnzzAnalytics}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${BLOG.analytics.cnzzAnalytics}' type='text/javascript'%3E%3C/script%3E"));
`
}}
/>
@@ -91,4 +109,4 @@ const ThirdPartyScript = () => {
</>)
}
export default ThirdPartyScript
export default CommonScript

View File

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

View File

@@ -1,15 +0,0 @@
import { useTheme } from '@/lib/theme'
import localStorage from 'localStorage'
const DarkModeButton = () => {
const { theme, changeTheme } = useTheme()
const handleChangeDarkMode = () => {
const newTheme = (theme === 'light' ? 'dark' : 'light')
changeTheme(newTheme)
localStorage.setItem('theme', newTheme)
}
return <div className='z-10 p-1 border hover:shadow-xl duration-200 dark:border-gray-500 mr-2 h-12 my-2 bg-white dark:bg-gray-600 dark:bg-opacity-70 bg-opacity-70 dark:hover:bg-gray-100 text-xl cursor-pointer dark:text-gray-300 dark:hover:text-black'>
<i className={'fa p-2.5 hover:scale-125 transform duration-200 ' + (theme === 'dark' ? ' fa-sun-o' : ' fa-moon-o') } onClick={handleChangeDarkMode} />
</div>
}
export default DarkModeButton

View File

@@ -1,28 +0,0 @@
import BLOG from '@/blog.config'
import React from 'react'
const Footer = ({ fullWidth = true }) => {
const d = new Date()
const y = d.getFullYear()
const from = +BLOG.since
return (
<footer
className='flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 text-sm text-gray-400'
>
<hr className='py-2'/>
<span className='fa fa-shield leading-6'> <a href='https://beian.miit.gov.cn/' className='ml-1'>闽ICP备20010331号</a></span>
<br />
<span className='fa fa-copyright leading-6'> {from === y || !from ? y : `${from} - ${y}`} {BLOG.author} </span>
<br />
<span id='busuanzi_container_site_pv' className='hidden'>
<a id='busuanzi_container_site_pv' href='https://www.cnzz.com/stat/website.php?web_id=1279970751' target='_blank'
className='fa fa-user' rel='noreferrer'> pv <span id='busuanzi_value_site_pv'></span></a>
</span>
<span id='busuanzi_container_site_uv' className='hidden'><span className='s'> | </span>
<a href='http://tongji.baidu.com/web/10000363165/overview/index?siteId=16809429' target='_blank' className='fa fa-eye' rel='noreferrer'> uv <span id='busuanzi_value_site_uv'></span></a>
</span>
</footer>
)
}
export default Footer

View File

@@ -0,0 +1,29 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function GoogleAdsense () {
const initGoogleAdsense = () => {
const ads = document.getElementsByClassName('adsbygoogle').length
const newAdsCount = ads
if (newAdsCount > 0) {
for (let i = 0; i <= newAdsCount; i++) {
try {
// eslint-disable-next-line no-undef
(adsbygoogle = window.adsbygoogle || []).push({})
} catch (e) {
}
}
}
}
const router = useRouter()
useEffect(() => {
initGoogleAdsense()
router.events.on('routeChangeComplete', initGoogleAdsense)
return () => {
router.events.off('routeChangeComplete', initGoogleAdsense)
}
}, [router.events])
return null
}

View File

@@ -5,12 +5,12 @@ import * as gtag from '@/lib/gtag'
const Gtag = () => {
const router = useRouter()
useEffect(() => {
const handleRouteChange = url => {
const gtagRouteChange = url => {
gtag.pageview(url)
}
router.events.on('routeChangeComplete', handleRouteChange)
router.events.on('routeChangeComplete', gtagRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
router.events.off('routeChangeComplete', gtagRouteChange)
}
}, [router.events])
return null

View File

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

View File

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

View File

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

View File

@@ -1,61 +0,0 @@
import React, { useState } from 'react'
import TocBar from '@/components/TocBar'
import throttle from 'lodash.throttle'
import ShareButton from '@/components/ShareButton'
import TopJumper from '@/components/TopJumper'
const RightAside = ({ toc, post }) => {
// 无目录就直接返回空
if (toc.length < 1) return <></>
// 监听滚动事件
React.useEffect(() => {
window.addEventListener('resize', resizeWindowHideToc)
return () => {
window.removeEventListener('resize', resizeWindowHideToc)
}
}, [])
const resizeWindowHideToc = throttle(() => {
if (window.innerWidth > 1300) {
changeHideAside(false)
} else {
changeHideAside(true)
}
}, 500)
const [hideAside, changeHideAside] = useState(true)
return <aside className='dark:bg-gray-800'>
{/* 上方菜单组 */}
<div
className={(hideAside ? 'right-0' : 'right-48') + ' z-20 space-x-2 fixed flex top-0 px-3 py-1 duration-500'}>
{/* 目录按钮 */}
<div
className='border dark:border-gray-500 my-2 bg-white dark:bg-gray-600 bg-opacity-70 dark:hover:bg-gray-100 text-xl cursor-pointer dark:text-gray-300 dark:hover:text-black p-1'>
<i className='fa fa-book p-2.5 hover:scale-125 transform duration-200'
onClick={() => changeHideAside(!hideAside)} />
</div>
</div>
{/* 下方菜单组 */}
<div
className={(hideAside ? 'right-0' : 'right-48') + ' space-x-2 fixed flex bottom-24 px-4 py-1 duration-500'}>
<div className='flex-wrap'>
{/* 分享按钮 */}
<ShareButton post={post} />
{/* 跳回顶部 */}
<TopJumper />
</div>
</div>
{/* 目录 */}
<section
className={(hideAside ? '-mr-48' : 'mr-0 shadow-xl xl:shadow-none') + ' md:static top-0 fixed h-full w-48 right-0 dark:bg-gray-800 duration-500 top-0'}>
<div className='sticky top-0'>
<TocBar toc={toc} />
</div>
</section>
</aside>
}
export default RightAside

View File

@@ -1,11 +0,0 @@
import React from 'react'
import TopJumper from '@/components/TopJumper'
import ShareButton from '@/components/ShareButton'
const RightWidget = ({ post }) => {
return <div className='flex-wrap'>
<ShareButton post={post} />
<TopJumper />
</div>
}
export default RightWidget

View File

@@ -1,86 +0,0 @@
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import React from 'react'
import { createPopper } from '@popperjs/core'
import copy from 'copy-to-clipboard'
import QRCode from 'qrcode.react'
const ShareBar = ({ post }) => {
const router = useRouter()
const shareUrl = BLOG.link + router.asPath
// 二维码悬浮
const [qrCodeShow, setQrCodeShow] = React.useState(false)
const btnRef = React.createRef()
const popoverRef = React.createRef()
const openPopover = () => {
createPopper(btnRef.current, popoverRef.current, {
placement: 'left'
})
setQrCodeShow(true)
}
const closePopover = () => {
setQrCodeShow(false)
}
const copyUrl = () => {
copy(shareUrl)
alert('当前链接已复制到剪贴板')
}
return <>
<div
className='dark:border-gray-500 py-2 text-gray-500 flex-col text-center space-y-2 w-12 border my-1 bg-white dark:bg-gray-800 dark:text-white overflow-visible'>
<div className='text-3xl cursor-pointer'>
<a className='fa fa-facebook-square'
href={`https://www.facebook.com/sharer.php?u=${shareUrl}`} />
</div>
<div className='text-3xl cursor-pointer'>
<a className='fa fa-twitter-square' target='_blank' rel='noreferrer'
href={`https://twitter.com/intent/tweet?title=${post.title}&url${shareUrl}`} />
</div>
<div className='text-3xl cursor-pointer'>
<a className='fa fa-telegram' href={`https://telegram.me/share/url?url=${shareUrl}&text=${post.title}`} />
</div>
<div className='cursor-pointer text-2xl'>
<a className='fa fa-wechat' ref={btnRef}
onMouseEnter={() => {
openPopover()
}}
onMouseLeave={() => {
closePopover()
}}>
<div ref={popoverRef}
className={(qrCodeShow ? 'animate__animated animate__fadeIn ' : 'hidden') + ' text-center py-2'}>
<div className='p-2 bg-white border-0 duration-200 transform block z-50 font-normal shadow-xl mr-10'>
<QRCode
value={shareUrl}// 生成二维码的内容
fgColor='#000000' // 二维码的颜色
/>
</div>
<span className='bg-white text-black font-semibold p-1 mb-0 rounded-t-lg text-sm mx-auto mr-10'>
扫一扫分享
</span>
</div>
</a>
</div>
<div className='cursor-pointer text-2xl'>
<a className='fa fa-weibo' target='_blank' rel='noreferrer'
href={`https://service.weibo.com/share/share.php?url=${shareUrl}&title=${post.title}`} />
</div>
<div className='cursor-pointer text-2xl'>
<a className='fa fa-qq' target='_blank' rel='noreferrer'
href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${post.title}&desc=${post.summary}`} />
</div>
<div className='cursor-pointer text-2xl'>
<a className='fa fa-star' target='_blank' rel='noreferrer'
href={`https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${shareUrl}&sharesource=qzone&title=${post.title}&summary=${post.summary}`} />
</div>
<div className='cursor-pointer text-2xl'>
<a className='fa fa-link' onClick={copyUrl} />
</div>
</div>
</>
}
export default ShareBar

View File

@@ -1,134 +0,0 @@
import Tags from '@/components/Tags'
import { useLocale } from '@/lib/locale'
import Link from 'next/link'
import BLOG from '@/blog.config'
import React, { useEffect, useState } from 'react'
import Router, { useRouter } from 'next/router'
import DarkModeButton from '@/components/DarkModeButton'
import Footer from '@/components/Footer'
import throttle from 'lodash.throttle'
import TocBar from '@/components/TocBar'
import SocialButton from '@/components/SocialButton'
const SideBar = ({ tags, currentTag, post }) => {
const locale = useLocale()
const router = useRouter()
const [searchValue, setSearchValue] = useState('')
const handleKeyUp = (e) => {
if (e.keyCode === 13) {
Router.push({ pathname: '/', query: { s: searchValue } })
}
}
// 监听resize事件
useEffect(() => {
window.addEventListener('resize', collapseSideBar)
collapseSideBar()
return () => {
window.removeEventListener('resize', collapseSideBar)
}
}, [])
const collapseSideBar = throttle(() => {
if (window.innerWidth > 1300) {
changeCollapse(false)
} else {
changeCollapse(true)
}
}, 500)
const [collapse, changeCollapse] = useState(true)
return <aside className='z-10'>
<div
className={(collapse ? '-ml-80 ' : 'shadow-2xl xl:shadow-none ') + ' dark:bg-gray-800 bg-white sidebar h-full w-72 duration-500 ease-in-out'}>
{/* Logo */}
<section className='flex border-b px-5 pt-8 pb-6 flex-col sticky top-0 bg-white dark:bg-gray-800 z-10'>
<Link href='/'>
<div
className='mx-auto text-center cursor-pointer text-3xl dark:bg-gray-900 dark:text-gray-300 font-semibold dark:hover:bg-gray-600 bg-gray-700 text-white p-2 hover:scale-105 hover:shadow-2xl duration-200 transform'>{BLOG.title}</div>
</Link>
<i className='mx-auto fa fa-map-marker pl-1 dark:text-gray-300 mt-5' >&nbsp;Fuzhou, China</i>
</section>
{/* 搜索框 */}
<section className={ (post ? ' ' : ' sticky top-36 ') + ' z-20 border-t border-b flex justify-center items-center py-5 pr-5 pl-2 bg-gray-100 dark:bg-black'}>
<input
type='text'
placeholder={
currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`
}
className='shadow-inner duration-200 pl-2 rounded w-full py-2 border dark:border-gray-600 bg-white text-black dark:bg-gray-700 dark:text-white'
onKeyUp={handleKeyUp}
onChange={e => setSearchValue(e.target.value)}
defaultValue={router.query.s ?? ''}
/>
<i className='fa fa-search text-gray-400 -ml-8' />
</section>
{/* wrapper */}
<div className={ (post ? ' ' : ' ') + 'px-6'}>
{/* 菜单 */}
<nav className='mt-6'>
<strong className='text-2xl text-gray-600 dark:text-gray-400'>菜单</strong>
<ul className='mt-4 leading-8 text-gray-500 dark:text-gray-400'>
<li><a className='fa fa-info hover:underline' href='/article/about' id='about'><span
className='ml-2'>关于本站</span></a></li>
<li><a className='fa fa-rss hover:underline' href='/feed' target='_blank' id='feed'><span
className='ml-2'>RSS订阅</span></a></li>
<li></li>
</ul>
</nav>
{/* 标签云 */}
<section className='mt-6'>
<strong className='text-2xl text-gray-600 dark:text-gray-400'>标签</strong>
<div className='mt-4'>
<Tags tags={tags} currentTag={currentTag} />
</div>
</section>
{/* 联系 */}
<section>
<div className='mt-6'>
<strong className='text-2xl text-gray-600 dark:text-gray-400'>联系我</strong>
<div className='mt-2 py-2'>
<SocialButton />
</div>
</div>
</section>
{/* 站点信息 */}
<section className='my-3'>
<Footer />
</section>
</div>
{post && (
<div className='sticky top-36'>
<TocBar toc={post.toc} />
</div>
)}
</div>
{/* 顶部菜单按钮 */}
<div
className={(collapse ? 'left-0' : 'left-72') + ' z-30 fixed flex flex-nowrap md:flex-col top-0 pl-4 py-1 duration-500 ease-in-out'}>
{/* 菜单折叠 */}
<div className='p-1 border hover:shadow-xl duration-200 dark:border-gray-500 h-12 bg-white dark:bg-gray-600 dark:bg-opacity-70 bg-opacity-70
dark:hover:bg-gray-100 text-xl cursor-pointer mr-2 my-2 dark:text-gray-300 dark:hover:text-black'>
<i className='fa fa-bars p-2.5 hover:scale-125 transform duration-200'
onClick={() => changeCollapse(!collapse)} />
</div>
{/* 夜间模式 */}
<DarkModeButton />
</div>
</aside>
}
export default SideBar

View File

@@ -1,24 +0,0 @@
import React from 'react'
const SocialButton = () => {
return <>
<div className='space-x-3 text-2xl text-gray-500 dark:text-gray-400'>
<a target='_blank' rel='noreferrer' title={'github'}
href={'https://github.com/tangly1024'} >
<div className='fa fa-github transform hover:scale-125 duration-150'/>
</a>
<a target='_blank' rel='noreferrer' title={'twitter'}
href={'https://twitter.com/troy1024_1'} >
<div className='fa fa-twitter transform hover:scale-125 duration-150'/>
</a>
<a href={'https://t.me/tangly_1024'} title={'telegram'} >
<div className='fa fa-telegram transform hover:scale-125 duration-150'/>
</a>
<a target='_blank' rel='noreferrer' title={'weibo'}
href={'https://weibo.com/tangly1024'} >
<div className='fa fa-weibo transform hover:scale-125 duration-150'/>
</a>
</div>
</>
}
export default SocialButton

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,21 @@
import BLOG from '@/blog.config'
import { useEffect } from 'react'
/**
* 评论插件
* @param issueTerm
* @param layout
* @returns {JSX.Element}
* @constructor
*/
const Utterances = ({ issueTerm, layout }) => {
useEffect(() => {
const theme =
BLOG.appearance === 'auto'
? 'preferred-color-scheme'
: BLOG.appearance === 'light'
? 'github-light'
: 'github-dark'
? 'github-light'
: 'github-dark'
const script = document.createElement('script')
const anchor = document.getElementById('comments')
script.setAttribute('src', 'https://utteranc.es/client.js')

View File

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 352 KiB

BIN
docs/screenshot-youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

View File

@@ -5,7 +5,7 @@
"@/*": ["./*"],
"@/components/*": ["components/*"],
"@/data/*": ["data/*"],
"@/layouts/*": ["layouts/*"],
"@/layouts/*": ["theme/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
}

View File

@@ -1,175 +0,0 @@
import TagItem from '@/components/TagItem'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import BLOG from '@/blog.config'
import formatDate from '@/lib/formatDate'
import 'gitalk/dist/gitalk.css'
import Comment from '@/components/Comment'
import Progress from '@/components/Progress'
import { useRef } from 'react'
import Image from 'next/image'
import RewardButton from '@/components/RewardButton'
import { useTheme } from '@/lib/theme'
import SideBar from '@/components/SideBar'
import BlogPostMini from '@/components/BlogPostMini'
import { useRouter } from 'next/router'
import ShareButton from '@/components/ShareButton'
import TopJumper from '@/components/TopJumper'
import CommonHead from '@/components/CommonHead'
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
const ArticleLayout = ({
children,
blockMap,
frontMatter,
emailHash,
fullWidth = true,
tags,
prev,
next
}) => {
const meta = {
title: frontMatter.title,
type: 'article'
}
const targetRef = useRef(null)
const { theme } = useTheme()
const url = BLOG.link + useRouter().asPath
return (
<div className={`${BLOG.font} ${theme}`}>
<CommonHead meta={meta} />
{/* live2d 看板娘 */}
<script async src='https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/autoload.js' />
<Progress targetRef={targetRef} />
{/* Wrapper */}
<div className='flex justify-between bg-gray-100 dark:bg-black'>
<SideBar tags={tags} post={frontMatter} />
{/* 主体区块 */}
<main className='bg-gray-100 dark:bg-black w-full'>
{/* 中央区域 wrapper */}
<div>
<header
className='hover:scale-105 hover:shadow-2xl duration-200 transform mx-auto max-w-5xl mt-20 md:flex-shrink-0 overflow-y-hidden animate__fadeIn animate__animated'>
{/* 封面图 */}
{frontMatter.page_cover && frontMatter.page_cover.length > 1 && (
<img className='bg-center object-cover w-full' style={{ maxHeight: '40rem' }}
src={frontMatter.page_cover} alt={frontMatter.title} />
)}
</header>
<article
ref={targetRef}
className='hover:shadow-2xl mb-20 overflow-x-auto px-10 py-10 max-w-5xl mx-auto bg-white dark:border-gray-700 dark:bg-gray-600'>
{/* 文章标题 */}
<h1 className='font-bold text-4xl text-black my-5 dark:text-white animate__animated animate__fadeIn'>
{frontMatter.title}
</h1>
{/* 文章信息 */}
<div className='justify-between flex flex-wrap bg-gray-50 p-2 dark:bg-gray-700 dark:text-white'>
<div className='flex-nowrap flex'>
{frontMatter.slug !== 'about' && (<>
<a
className='hidden md:block duration-200 px-1' href='/article/about'
>
<Image alt={BLOG.author} width={33} height={33} src='/avatar.svg'
className='rounded-full cursor-pointer transform hover:scale-125 duration-200' />
</a>
</>)}
{frontMatter.tags && (
<div className='flex flex-nowrap leading-8 p-1'>
{frontMatter.tags.map(tag => (
<TagItem key={tag} tag={tag} />
))}
</div>
)}
{frontMatter.type[0] !== 'Page' && (
<div className='flex items-start text-gray-500 dark:text-gray-400 leading-10'>
{formatDate(
frontMatter?.date?.start_date || frontMatter.createdTime,
BLOG.lang
)}
</div>
)}
</div>
{/* 不蒜子 */}
<div id='busuanzi_container_page_pv' className='hidden'>
<a href='https://analytics.google.com/analytics/web/#/p273013569/reports/reportinghub'
className='fa fa-eye text-gray-500 text-sm leading-none py-1 px-2'>
&nbsp;<span id='busuanzi_value_page_pv' className='leading-6'></span>
</a>
</div>
</div>
{/* Notion文章主体 */}
{blockMap && (
<NotionRenderer recordMap={blockMap} mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
<div className='flex justify-center pt-5'>
<RewardButton />
</div>
<p className='flex justify-center py-5'>
- 💖 😚 💖 -
</p>
{/* 版权声明 */}
<section
className='overflow-auto dark:bg-gray-700 dark:text-gray-300 bg-gray-100 p-5 leading-8 border-l-4 border-red-500'>
<ul>
<li><strong>本文作者</strong>{BLOG.author}</li>
<li><strong>本文链接</strong> <a href={url}>{url}</a> {frontMatter.title}</li>
<li><strong>版权声明</strong> BY-NC-SA </li>
</ul>
</section>
<div className='text-gray-800 my-5 dark:text-gray-300'>
<div className='mt-4 my-2 font-bold'>继续阅读</div>
<div className='flex flex-wrap lg:flex-nowrap lg:space-x-10 justify-between py-2'>
<BlogPostMini post={prev} />
<BlogPostMini post={next} />
</div>
</div>
{/* 评论互动 */}
<Comment frontMatter={frontMatter} />
</article>
</div>
</main>
{/* 下方菜单组 */}
<div
className='right-0 space-x-2 fixed flex bottom-24 px-5 py-1 duration-500'>
<div className='flex-wrap'>
{/* 分享按钮 */}
<ShareButton post={frontMatter} />
{/* 跳回顶部 */}
<TopJumper />
</div>
</div>
</div>
</div>
)
}
export default ArticleLayout

View File

@@ -1,130 +0,0 @@
import BlogPost from '@/components/BlogPost'
import PropTypes from 'prop-types'
import Pagination from '@/components/Pagination'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { useTheme } from '@/lib/theme'
import { useEffect, useState } from 'react'
import SideBar from '@/components/SideBar'
import throttle from 'lodash.throttle'
import CommonHead from '@/components/CommonHead'
const DefaultLayout = ({ tags, posts, page, currentTag, ...customMeta }) => {
const meta = {
title: BLOG.title,
type: 'website',
...customMeta
}
page = page ?? 1
let postsToShow = []
let filteredBlogPosts = posts ?? []
let currentSearch = ''
if (posts) {
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)
if (posts) {
postsToShow = filteredBlogPosts.slice(
BLOG.postsPerPage * (page - 1),
BLOG.postsPerPage * page
)
}
let showNext = false
if (filteredBlogPosts) {
const totalPosts = filteredBlogPosts.length
showNext = page * BLOG.postsPerPage < totalPosts
}
useEffect(() => {
// 首页隐藏看板娘
// const ref = document.getElementById('waifu')
// if (ref) {
// ref.remove()
// }
window.addEventListener('resize', changeColumnCount)
changeColumnCount()
return () => {
window.removeEventListener('resize', changeColumnCount)
}
}, [])
const changeColumnCount = throttle(() => {
if (window.innerWidth > 2500) {
changeColumn(5)
} else if (window.innerWidth > 1800) {
changeColumn(4)
} else if (window.innerWidth > 1300) {
changeColumn(3)
} else if (window.innerWidth > 900) {
changeColumn(2)
} else if (window.innerWidth <= 900) {
changeColumn(1)
}
}, 500)
const [column, changeColumn] = useState(3)
const { theme } = useTheme()
return (
<div id='wrapper' className={theme}>
<CommonHead meta={meta} />
<div className={`${BLOG.font} flex bg-gray-100 dark:bg-black min-h-screen`}>
{/* 侧边菜单 */}
<SideBar tags={tags} currentTag={currentTag} />
<main className='md:px-24 p-5 flex-grow'>
{(!page || page === 1) && (<div className='py-5' />)}
{/* 标签 */}
{currentTag && (
<div className='pb-5 dark:text-gray-200'>
<div className='py-1'>标签: {currentTag}</div>
<hr />
</div>
)}
{/* 当前搜索 */}
{(currentSearch || (page && page !== 1)) && (
<div className='pb-5'>
<div className='dark:text-gray-200 flex justify-between py-1'>
{currentSearch && (<span>搜索关键词: {currentSearch}</span>)}
{page && page !== 1 && (<span> {page} / {totalPages}</span>)}
</div>
<hr />
</div>
)}
<div className='mx-auto'>
{/* 文章列表 */}
<div style={{ columnCount: column }}>
{!postsToShow.length && (
<p className='text-gray-500 dark:text-gray-300'>No posts found.</p>
)}
{postsToShow.map(post => (
<BlogPost key={post.id} post={post} tags={tags} />
))}
</div>
<Pagination page={page} showNext={showNext} />
</div>
</main>
</div>
</div>
)
}
DefaultLayout.propTypes = {
posts: PropTypes.array.isRequired,
tags: PropTypes.object.isRequired,
currentTag: PropTypes.string
}
export default DefaultLayout

104
lib/busuanzi.js Normal file
View File

@@ -0,0 +1,104 @@
/* eslint-disable */
let bszCaller, bszTag, scriptTag, ready
let t; let e; let n; let a = !1
let c = []
// 修复Node同构代码的问题
if (typeof document !== 'undefined') {
ready = function (t) {
return a || document.readyState === 'interactive' || document.readyState === 'complete'
? t.call(document)
: c.push(function () {
return t.call(this)
}), this
}, e = function () {
for (let t = 0, e = c.length; t < e; t++) c[t].apply(document)
c = []
}, n = function () {
a || (a = !0, e.call(window),
document.removeEventListener ? document.removeEventListener('DOMContentLoaded', n, !1) : document.attachEvent && (document.detachEvent('onreadystatechange', n), window == window.top && (clearInterval(t), t = null)))
}, document.addEventListener
? document.addEventListener('DOMContentLoaded', n, !1)
: document.attachEvent && (document.attachEvent('onreadystatechange', function () {
/loaded|complete/.test(document.readyState) && n()
}), window == window.top && (t = setInterval(function () {
try {
a || document.documentElement.doScroll('left')
} catch (t) {
return
}
n()
}, 5)))
}
bszCaller = {
fetch: function (t, e) {
const n = 'BusuanziCallback_' + Math.floor(1099511627776 * Math.random())
t = t.replace('=BusuanziCallback', '=' + n)
scriptTag = document.createElement('SCRIPT'), scriptTag.type = 'text/javascript', scriptTag.defer = !0, scriptTag.src = t, scriptTag.referrerPolicy = "no-referrer-when-downgrade", document.getElementsByTagName('HEAD')[0].appendChild(scriptTag)
window[n] = this.evalCall(e)
},
evalCall: function (e) {
return function (t) {
ready(function () {
try {
e(t), scriptTag && scriptTag.parentElement && scriptTag.parentElement.removeChild && scriptTag.parentElement.removeChild(scriptTag)
} catch (t) {
// console.log(t), bszTag.hides()
}
})
}
}
}
const fetch = () => {
bszTag && bszTag.hides()
bszCaller.fetch('//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback', function (t) {
// console.log('不蒜子请求结果',t)
bszTag.texts(t), bszTag.shows()
})
}
bszTag = {
bszs: ['site_pv', 'page_pv', 'site_uv'],
texts: function (n) {
this.bszs.map(function (t) {
const e = document.getElementsByClassName('busuanzi_value_' + t)
if(e){
for (var element of e) {
element.innerHTML = n[t]
}
}
})
},
hides: function () {
this.bszs.map(function (t) {
const e = document.getElementsByClassName('busuanzi_container_' + t)
if(e){
for (var element of e){
element.style.display = 'none'
}
}
})
},
shows: function () {
this.bszs.map(function (t) {
const e = document.getElementsByClassName('busuanzi_container_' + t)
if(e){
for(var element of e){
element.style.display = 'inline'
}
}
})
}
}
// 修复Node同构代码的问题
if (typeof document !== 'undefined') {
fetch()
}
module.exports = {
fetch
}

View File

@@ -1,21 +1,26 @@
import { getCacheFromFile, setCacheToFile } from '@/lib/cache/local_file_cache'
import { getCacheFromMemory, setCacheToMemory } from '@/lib/cache/memory_cache'
import BLOG from '@/blog.config'
// import { getCacheFromFile, setCacheToFile } from './local_file_cache'
const enableCache = true // 生产环境禁用
/**
* 为减少频繁接口请求notion数据将被缓存
* @param {*} key
* @returns
*/
export async function getDataFromCache (key) {
let dataFromCache
if (BLOG.isProd) {
dataFromCache = await getCacheFromMemory(key)
} else {
dataFromCache = await getCacheFromFile(key)
if (!enableCache) {
return null
}
const dataFromCache = await getCacheFromMemory(key)
if (JSON.stringify(dataFromCache) === '[]') {
return null
}
return dataFromCache
}
export async function setDataToCache (key, data) {
if (BLOG.isProd) {
await setCacheToMemory(key, data)
} else {
await setCacheToFile(key, data)
if (!enableCache || !data) {
return
}
await setCacheToMemory(key, data)
}

View File

@@ -1,5 +1,4 @@
import fs from 'fs'
import BLOG from '@/blog.config'
const path = require('path')
// 文件缓存持续10秒
@@ -11,7 +10,14 @@ export async function getCacheFromFile (key) {
const exist = await fs.existsSync(jsonFile)
if (!exist) return null
const data = await fs.readFileSync(jsonFile)
const json = data ? JSON.parse(data) : {}
let json = null
if (!data) return null
try {
json = JSON.parse(data)
} catch (error) {
console.error('读取JSON缓存文件失败', data)
return null
}
// 缓存超过有效期就作废
const cacheValidTime = new Date(parseInt(json[key + '_expire_time']) + cacheInvalidSeconds)
const currentTime = new Date()

View File

@@ -1,9 +1,12 @@
import cache from 'memory-cache'
import BLOG from 'blog.config'
export async function getCacheFromMemory (key, options) { // url为缓存标识
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)
}
export async function setCacheToMemory (key, data) { // url为缓存标识
await cache.put(key, data, 60 * 1000)
export async function setCacheToMemory (key, data) {
await cache.put(key, data, cacheTime * 1000)
}

View File

@@ -1,8 +1,36 @@
export default function formatDate(date, local) {
/**
* 格式化日期
* @param date
* @param local
* @returns {string}
*/
export default function formatDate (date, local) {
const d = new Date(date)
const options = { year: 'numeric', month: 'short', day: 'numeric' }
const res = d.toLocaleDateString(local, options)
return local.slice(0, 2).toLowerCase() === 'zh'
? res.replace('年', '').replace('月', '').replace('日', '')
? res.replace('年', '-').replace('月', '-').replace('日', '')
: res
}
export function formatDateFmt (timestamp, fmt) {
const date = new Date(timestamp)
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds() // 毫秒
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
}
}
return fmt.trim()
}

111
lib/global.js Normal file
View File

@@ -0,0 +1,111 @@
import lang from './lang'
import { useContext, createContext, useState, useEffect } from 'react'
import Router from 'next/router'
import { initTheme, loadUserThemeFromCookies } from './theme'
const GlobalContext = createContext()
/**
* 全局变量Provider包括语言本地化、样式主题、搜索词
* @param children
* @returns {JSX.Element}
* @constructor
*/
export function GlobalContextProvider ({ children }) {
const [locale, changeLocale] = useState(generateLocaleDict('en-US'))
const [theme, changeTheme] = useState(loadUserThemeFromCookies())
const [onLoading, changeLoadingState] = useState(false)
Router.events.on('routeChangeStart', (...args) => {
changeLoadingState(true)
})
Router.events.on('routeChangeComplete', (...args) => {
changeLoadingState(false)
})
// 服务端静态渲染在渲染hooks后根据前端变量做初始化工作
useEffect(() => {
initTheme(theme, changeTheme)
initLocale(locale, changeLocale)
})
return (
<GlobalContext.Provider value={{ onLoading, locale, theme, changeTheme }}>
{children}
</GlobalContext.Provider>
)
}
/**
* 获取当前语言字典
* @returns 不同语言对应字典
*/
const generateLocaleDict = (langString) => {
let userLocale = lang['en-US']
if (!langString) {
return userLocale
}
if (langString.slice(0, 2).toLowerCase() === 'zh') {
switch (langString.toLowerCase()) {
case 'zh-cn':
case 'zh-sg':
userLocale = lang['zh-CN']
break
case 'zh-hk':
userLocale = lang['zh-HK']
break
case 'zh-tw':
userLocale = lang['zh-TW']
break
default:
userLocale = lang['zh-CN']
}
}
const resLocale = mergeDeep({}, lang['en-US'], userLocale)
return resLocale
}
/**
* 初始化语言
* 根据用户当前浏览器语言进行切换
*/
const initLocale = (locale, changeLocale) => {
if (window) {
const targetLocale = generateLocaleDict(window.navigator.language)
if (JSON.stringify(locale) !== JSON.stringify(targetLocale)) {
changeLocale(targetLocale)
}
}
}
/**
* 深度合并两个对象
* @param target
* @param sources
*/
export function mergeDeep (target, ...sources) {
if (!sources.length) return target
const source = sources.shift()
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} })
mergeDeep(target[key], source[key])
} else {
Object.assign(target, { [key]: source[key] })
}
}
}
return mergeDeep(target, ...sources)
}
/**
* 对象检查
* @param item
* @returns {boolean}
*/
export function isObject (item) {
return (item && typeof item === 'object' && !Array.isArray(item))
}
export const useGlobal = () => useContext(GlobalContext)

View File

@@ -1,83 +1,12 @@
import zhCN from './lang/zh-CN'
import enUS from './lang/en-US'
import zhHK from './lang/zh-HK'
import zhTW from './lang/zh-TW'
const lang = {
en: {
NAV: {
INDEX: 'Blog',
RSS: 'RSS',
SEARCH: 'Search',
ABOUT: 'About',
NAVGATION: 'NAVGATION'
},
PAGINATION: {
PREV: 'Prev',
NEXT: 'Next'
},
SEARCH: {
ARTICLES: 'Search Articles',
TAGS: 'Search in'
},
POST: {
BACK: 'Back',
TOP: 'Top'
}
},
'zh-CN': {
NAV: {
INDEX: '首页',
RSS: '订阅',
SEARCH: '搜索',
ABOUT: '关于',
NAVGATION: '导航'
},
PAGINATION: {
PREV: '上一页',
NEXT: '下一页'
},
SEARCH: {
ARTICLES: '搜索文章',
TAGS: '搜索标签'
},
POST: {
BACK: '返回上页',
TOP: '回到顶部'
}
},
'zh-HK': {
NAV: {
INDEX: '網誌',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
NAVGATION: '導航'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
SEARCH: {
ARTICLES: '搜尋文章',
TAGS: '搜尋標簽'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
},
'zh-TW': {
NAV: {
INDEX: '部落格',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
NAVGATION: '導航'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
}
'en-US': enUS,
'zh-CN': zhCN,
'zh-HK': zhHK,
'zh-TW': zhTW
}
export default lang

45
lib/lang/en-US.js Normal file
View File

@@ -0,0 +1,45 @@
export default {
LOCALE: 'en-US',
NAV: {
INDEX: 'Blog',
RSS: 'RSS',
SEARCH: 'Search',
ABOUT: 'About',
MAIL: 'E-Mail',
ARCHIVE: 'Archive'
},
COMMON: {
MORE: 'More',
NO_MORE: 'No More',
LATEST_POSTS: 'Latest posts',
TAGS: 'Tags',
NO_TAG: 'NoTag',
CATEGORY: 'Category',
SHARE: 'Share',
SCAN_QR_CODE: 'Scan QRCode',
URL_COPIED: 'URL has copied!',
TABLE_OF_CONTENTS: 'Table of Contents',
RELATE_POSTS: 'Relate Posts',
COPYRIGHT: 'Copyright',
AUTHOR: 'Author',
URL: 'URL',
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!',
RESULT_OF_SEARCH: 'Results Found',
ARTICLE_DETAIL: 'Article Details'
},
PAGINATION: {
PREV: 'Prev',
NEXT: 'Next'
},
SEARCH: {
ARTICLES: 'Search Articles',
TAGS: 'Search in'
},
POST: {
BACK: 'Back',
TOP: 'Top'
}
}

47
lib/lang/zh-CN.js Normal file
View File

@@ -0,0 +1,47 @@
export default {
LOCALE: 'zh-CN',
NAV: {
INDEX: '首页',
RSS: '订阅',
SEARCH: '搜索',
ABOUT: '关于',
NAVIGATOR: '导航',
MAIL: '邮箱',
ARCHIVE: '归档'
},
COMMON: {
MORE: '更多',
NO_MORE: '没有更多了',
LATEST_POSTS: '最新文章',
TAGS: '标签',
NO_TAG: 'NoTag',
CATEGORY: '分类',
SHARE: '分享',
SCAN_QR_CODE: '扫一扫二维码',
URL_COPIED: '链接已复制!',
TABLE_OF_CONTENTS: '目录',
RELATE_POSTS: '相关文章',
COPYRIGHT: '声明',
AUTHOR: '作者',
URL: '链接',
ANALYTICS: '统计',
POSTS: '篇文章',
VISITORS: '位访客',
VIEWS: '次查看',
COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。',
RESULT_OF_SEARCH: '篇搜索到的结果',
ARTICLE_DETAIL: '文章详情'
},
PAGINATION: {
PREV: '上一页',
NEXT: '下一页'
},
SEARCH: {
ARTICLES: '搜索文章',
TAGS: '搜索标签'
},
POST: {
BACK: '返回上页',
TOP: '回到顶部'
}
}

21
lib/lang/zh-HK.js Normal file
View File

@@ -0,0 +1,21 @@
export default {
NAV: {
INDEX: '網誌',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
MAIL: '電郵'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
SEARCH: {
ARTICLES: '搜尋文章',
TAGS: '搜尋標簽'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
}

21
lib/lang/zh-TW.js Normal file
View File

@@ -0,0 +1,21 @@
export default {
NAV: {
INDEX: '部落格',
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
MAIL: '電郵'
},
PAGINATION: {
PREV: '上一頁',
NEXT: '下一頁'
},
SEARCH: {
ARTICLES: '搜尋文章',
TAGS: '搜尋標簽'
},
POST: {
BACK: '返回',
TOP: '回到頂端'
}
}

View File

@@ -1,34 +0,0 @@
import BLOG from '@/blog.config'
import lang from './lang'
import { useContext, createContext } from 'react'
let locale = {}
if (BLOG.lang.slice(0, 2).toLowerCase() === 'zh') {
switch (BLOG.lang.toLowerCase()) {
case 'zh-cn':
case 'zh-sg':
locale = lang['zh-CN']
break
case 'zh-hk':
locale = lang['zh-HK']
break
case 'zh-tw':
locale = lang['zh-TW']
break
default:
locale = lang['zh-TW']
break
}
} else {
locale = lang.en
}
const LocaleContext = createContext()
export function LocaleProvider({ children }) {
return (
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
)
}
export const useLocale = () => useContext(LocaleContext)

View File

@@ -1,3 +1,4 @@
export { getAllPosts } from './notion/getAllPosts'
export { getAllTags } from './notion/getAllTags'
export { getPostBlocks } from './notion/getPostBlocks'
export { getAllCategories } from './notion/getAllCategories'

View File

@@ -0,0 +1,22 @@
/**
* 获取所有文章的分类
* @param allPosts
* @returns {Promise<{}|*[]>}
*/
export async function getAllCategories (allPosts) {
if (!allPosts) {
return []
}
let categories = allPosts.map(p => p.category)
categories = [...categories.flat()]
const categoryObj = {}
categories.forEach(category => {
if (category in categoryObj) {
categoryObj[category]++
} else {
categoryObj[category] = 1
}
})
return categoryObj
}

View File

@@ -1,6 +1,9 @@
import { idToUuid } from 'notion-utils'
export default function getAllPageIds (collectionQuery, viewId) {
if (!collectionQuery) {
return []
}
const views = Object.values(collectionQuery)[0]
if (!views) {
return []
@@ -12,7 +15,8 @@ export default function getAllPageIds (collectionQuery, viewId) {
} else {
const pageSet = new Set()
Object.values(views).forEach(view => {
view?.blockIds?.forEach(id => pageSet.add(id))
view?.blockIds?.forEach(id => pageSet.add(id)) // group视图
view?.collection_group_results?.blockIds?.forEach(id => pageSet.add(id)) // table视图
})
pageIds = [...pageSet]
}

View File

@@ -1,101 +1,80 @@
import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils'
import getAllPageIds from './getAllPageIds'
import getPageProperties from './getPageProperties'
import { defaultMapImageUrl } from 'react-notion-x'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getPostBlocks } from '@/lib/notion/getPostBlocks'
import { getNotionPageData } from '@/lib/notion/getNotionData'
export async function getAllPosts () {
const data = await getDataFromCache('posts_list')
if (data) {
return data
/**
* 获取所有文章列表
* @param notionPageData
* @param from
* @param includePage 是否包含Page类型
* @returns {Promise<*[]>}
*/
export async function getAllPosts ({ notionPageData, from, includePage = false }) {
if (!notionPageData) {
notionPageData = await getNotionPageData({ from })
}
let id = BLOG.notionPageId
const pageRecordMap = await getPostBlocks(id)
if (!pageRecordMap) {
return <>获取数据异常</>
if (!notionPageData) {
return []
}
id = idToUuid(id)
const collection = Object.values(pageRecordMap.collection)[0]?.value
const collectionQuery = pageRecordMap.collection_query
const block = pageRecordMap.block
const schema = collection?.schema
const pageBlock = notionPageData.block
const schema = notionPageData.schema
const tagOptions = notionPageData.tagOptions
const collectionQuery = notionPageData.collectionQuery
const rawMetadata = block[id].value
const data = []
const pageIds = getAllPageIds(collectionQuery)
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
properties.page_cover = getPostCover(id, pageBlock) ?? null
properties.content = pageBlock[id].value?.content ?? []
properties.tagItems = properties?.tags?.map(tag => {
return { name: tag, color: tagOptions.find(t => t.value === tag)?.color || 'gray' }
}) || []
delete properties.content
data.push(properties)
}
// Check Type 兼容Page-Database和Inline-Database
if (rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view') {
console.warn(`pageId "${id}" is not a database`)
return null
} else {
// Construct Data
const pageIds = getAllPageIds(collectionQuery)
const data = []
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const properties = (await getPageProperties(id, block, schema)) || null
// Add fullwidth, createdtime to properties
properties.createdTime = new Date(
block[id].value?.created_time
).toString()
properties.fullWidth = block[id].value?.format?.page_full_width ?? false
properties.page_cover = getPostCover(id, block, pageRecordMap) ?? getContentFirstImage(id, block, pageRecordMap)
properties.content = block[id].value?.content ?? []
data.push(properties)
}
// remove all the the items doesn't meet requirements
const posts = data.filter(post => {
// remove all the the items doesn't meet requirements
const posts = data.filter(post => {
if (includePage) {
return (
post.title &&
post.slug &&
post.title && post.slug &&
post?.status?.[0] === 'Published' &&
(post?.type?.[0] === 'Post' || post?.type?.[0] === 'Page')
)
})
} else {
return (
post.title && post.slug &&
post?.status?.[0] === 'Published' &&
(post?.type?.[0] === 'Post')
)
}
})
// Sort by date
if (BLOG.sortByDate) {
posts.sort((a, b) => {
const dateA = new Date(a?.date?.start_date || a.createdTime)
const dateB = new Date(b?.date?.start_date || b.createdTime)
return dateB - dateA
})
}
if (posts) {
await setDataToCache('posts_list', posts)
}
return posts
// Sort by date
if (BLOG.sortByDate) {
posts.sort((a, b) => {
const dateA = new Date(a?.date?.start_date || a.createdTime)
const dateB = new Date(b?.date?.start_date || b.createdTime)
return dateB - dateA
})
}
return posts
}
// 从Block获取封面图;优先取PageCover否则取内容图片
function getPostCover (id, block, pageRecordMap) {
function getPostCover (id, block) {
const pageCover = block[id].value?.format?.page_cover
if (pageCover) {
if (pageCover.startsWith('/')) return 'https://www.notion.so' + pageCover
if (pageCover.startsWith('http')) return defaultMapImageUrl(pageCover, block[id].value)
}
}
// 取文章的第一个图片内容作为封面
function getContentFirstImage (id, block, pageRecordMap) {
const pageBlock = block[id]?.value
const contentBlockId = pageBlock?.content?.find((blockId) => {
const block = pageRecordMap.block[blockId]?.value
if (block?.type === 'image') {
return true
}
})
if (contentBlockId) {
const contentBlock = pageRecordMap.block[contentBlockId]?.value
const source = contentBlock.properties?.source?.[0]?.[0] ??
contentBlock.format?.display_source
return defaultMapImageUrl(source, contentBlock)
}
return ''
}

View File

@@ -1,16 +1,20 @@
import { getAllPosts } from './getAllPosts'
export async function getAllTags (posts) {
if (!posts) {
const response = await getAllPosts()
posts = response.filter(
post =>
post.status[0] === 'Published' && post.type[0] === 'Post' && post.tags
)
/**
* 获取所有文章的标签
* @param allPosts
* @param sliceCount 默认截取数量为12若为0则返回全部
* @param tagOptions tags的下拉选项
* @returns {Promise<{}|*[]>}
*/
export async function getAllTags ({ allPosts, sliceCount = 16, tagOptions }) {
if (!allPosts) {
return []
}
let tags = posts.map(p => p.tags)
let tags = allPosts.map(p => p.tags)
tags = [...tags.flat()]
// 标签计数
const tagObj = {}
tags.forEach(tag => {
if (tag in tagObj) {
@@ -19,5 +23,16 @@ export async function getAllTags (posts) {
tagObj[tag] = 1
}
})
return tagObj
// 按照标签数量排序
const list = Object.keys(tagObj).map((tag) => {
const color = tagOptions.find(option => option.value === tag)?.color || 'gray'
return { name: tag, count: tagObj[tag], color }
})
list.sort((a, b) => b.count - a.count)
if (sliceCount && sliceCount > 0) {
return list.slice(0, sliceCount)
} else {
return list
}
}

View File

@@ -1,4 +1,4 @@
export default function getMetadata(rawMetadata) {
export default function getMetadata (rawMetadata) {
const metadata = {
locked: rawMetadata?.format?.block_locked,
page_full_width: rawMetadata?.format?.page_full_width,

121
lib/notion/getNotionData.js Normal file
View File

@@ -0,0 +1,121 @@
import BLOG from '@/blog.config'
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
* @param latestPostCount 截取最新文章数量
* @param tagsCount 截取标签数量
* @param includePage 是否包含PAGE类型
* @returns { allPosts: '文章列表', latestPosts ’最新文章, categories分类列表 postCount:'文章总数'tags:'标签列表' }
* 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数据
* @param pageId
* @param from 请求来源
* @returns {Promise<JSX.Element|*|*[]>}
*/
export async function getNotionPageData ({ pageId, from }) {
// 尝试从缓存获取
const cacheKey = 'page_record_map_' + pageId
const data = await getDataFromCache(cacheKey)
if (data) {
console.log('[请求缓存]:', `from:${from}`, `id:${pageId}`)
return data
}
const pageRecordMap = await getPageRecordMapByNotionAPI({ pageId, from })
// 存入缓存
if (pageRecordMap) {
await setDataToCache(cacheKey, pageRecordMap)
}
return pageRecordMap
}
/**
* 获取标签选项
* @param schema
* @returns {undefined}
*/
function getTagOptions (schema) {
const tagSchema = Object.values(schema).find(e => e.name === 'tags')
return tagSchema?.options || {}
}
/**
* 调用NotionAPI获取Page数据
* @returns {Promise<JSX.Element|null|*>}
*/
async function getPageRecordMapByNotionAPI ({ pageId, from }) {
const pageRecordMap = await getPostBlocks(pageId, from)
if (!pageRecordMap) {
return []
}
pageId = idToUuid(pageId)
const collection = Object.values(pageRecordMap.collection)[0]?.value
const collectionQuery = pageRecordMap.collection_query
const block = pageRecordMap.block
const schema = collection?.schema
const rawMetadata = block[pageId].value
const tagOptions = getTagOptions(schema)
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.warn(`pageId "${pageId}" is not a database`)
return null
}
return {
collection,
collectionQuery,
block,
schema,
tagOptions,
rawMetadata
}
}

View File

@@ -5,8 +5,7 @@ const indentLevels = {
sub_sub_header: 2
}
export const getPageTableOfContents = (page,recordMap)=> {
export const getPageTableOfContents = (page, recordMap) => {
// 获取 header sub_header sub_sub_header
const toc = (page.content ?? [])
.map((blockId) => {
@@ -66,4 +65,4 @@ export const getPageTableOfContents = (page,recordMap)=> {
}
return toc
}
}

View File

@@ -2,16 +2,75 @@ import BLOG from '@/blog.config'
import { NotionAPI } from 'notion-client'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
export async function getPostBlocks (id) {
let pageBlock = await getDataFromCache('page_block_' + id)
export async function getPostBlocks (id, from, slice) {
const cacheKey = 'page_block_' + id
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
return pageBlock
console.log('[请求缓存]:', `from:${from}`, `id:${id}`)
return filterPostBlocks(id, pageBlock, slice)
}
const authToken = BLOG.notionAccessToken || null
const api = new NotionAPI({ authToken })
pageBlock = await api.getPage(id)
try {
console.log('[请求API]:', `from:${from}`, `id:${id}`)
pageBlock = await api.getPage(id)
console.log('[请求成功]', `from:${from}`, `id:${id}`)
} catch (error) {
console.error('[请求失败]', `from:${from}`, `id:${id}`, `error:${error}`)
return null
}
if (pageBlock) {
await setDataToCache('page_block_' + id, pageBlock)
await setDataToCache(cacheKey, pageBlock)
return filterPostBlocks(id, pageBlock, slice)
}
return pageBlock
}
/**
*
* @param {*} id 页面ID
* @param {*} pageBlock 页面元素
* @param {*} slice 截取数量
* @returns
*/
function filterPostBlocks (id, pageBlock, slice) {
const clonePageBlock = deepClone(pageBlock)
let count = 0
for (const i in clonePageBlock?.block) {
const b = clonePageBlock?.block[i]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[i]
continue
}
count++
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 (id === BLOG.notionPageId) {
return clonePageBlock
}
return clonePageBlock
}
function deepClone (obj) {
const newObj = Array.isArray(obj) ? [] : {}
if (obj && typeof obj === 'object') {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key]
}
}
}
return newObj
}

View File

@@ -1,7 +1,7 @@
import { Feed } from 'feed'
import BLOG from '@/blog.config'
export function generateRss(posts) {
export function generateRss (posts) {
const year = new Date().getFullYear()
const feed = new Feed({
title: BLOG.title,
@@ -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

@@ -1,19 +1,44 @@
import { useContext, createContext, useState, useEffect } from 'react'
import localStorage from 'localStorage'
import cookie from 'react-cookies'
const ThemeContext = createContext()
export function ThemeProvider ({ children }) {
// 初始值
const defaultTheme = localStorage.getItem('theme')
const [theme, changeTheme] = useState()
useEffect(() => {
changeTheme(defaultTheme)
})
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>{children}</ThemeContext.Provider>
)
/**
* 初始化主题
* @param theme 用户默认主题state
* @param changeTheme 更改主题ChangeState函数
* @description 读取cookie中存的用户主题
*/
export const initTheme = (theme, changeTheme) => {
// 若未指定主题,则从时间和浏览器偏好中决定初始主题
if (!theme) {
const date = new Date()
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
const useDark = prefersDarkMode || (date.getHours() >= 18 || date.getHours() < 6)
if (useDark) {
theme = 'dark'
} else {
theme = 'light'
}
}
if (typeof window !== 'undefined') {
const htmlElement = document.getElementsByTagName('html')
htmlElement.className = ''
changeTheme(theme)
saveTheme(theme)
htmlElement.classList?.add(theme)
}
}
export const useTheme = () => useContext(ThemeContext)
/**
* 读取默认主题
* @returns {*}
*/
export const loadUserThemeFromCookies = () => {
return cookie.load('theme')
}
/**
* 保存默认主题
* @param newTheme
*/
export const saveTheme = (newTheme) => {
cookie.save('theme', newTheme, { path: '/' })
}

View File

@@ -3,9 +3,9 @@ module.exports = {
webpack5: true
},
images: {
domains: ['gravatar.com']
domains: ['gravatar.com', 'www.notion.so', 'avatars.githubusercontent.com']
},
async headers() {
async headers () {
return [
{
source: '/:path*{/}?',

View File

@@ -13,30 +13,37 @@
"url": "http://tangly1024.com"
},
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"dev": "next dev",
"build": "next build && next-sitemap --config next-sitemap.config.js",
"start": "next start",
"postbuild": "next-sitemap --config next-sitemap.config.js"
"post-build": "next-sitemap --config next-sitemap.config.js"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@popperjs/core": "^2.9.3",
"animate.css": "^4.1.1",
"axios": ">=0.21.1",
"copy-to-clipboard": "^3.3.1",
"feed": "^4.2.2",
"font-awesome": "^4.7.0",
"gitalk": "^1.7.2",
"localStorage": "^1.0.4",
"lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0",
"next": "10.2.0",
"notion-client": "^4.9.3",
"notion-utils": "4.8.6",
"preact": "^10.5.13",
"next": "^12.0.5",
"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.9.1",
"react-notion-x": "4.13.0",
"smoothscroll-polyfill": "^0.4.4",
"typed.js": "^2.0.12",
"use-ackee": "^3.0.0"
},
"devDependencies": {
@@ -48,7 +55,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"next-sitemap": "^1.6.102",
"next-sitemap": "^1.6.203",
"postcss": "^8.2.15",
"tailwindcss": "^2.1.2"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,31 +1,11 @@
import { Layout404 } from '@/themes'
/**
* 自定义404界面
* @returns {JSX.Element}
* @constructor
*/
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function Custom404 () {
const route = useRouter()
if (route.asPath.indexOf('/article') < 0 && route.asPath.indexOf('/404') < 0) {
// article 重定向,处理旧文章链接迁移。
const redirectUrl = '/article' + route.asPath
route.push(redirectUrl)
} else {
useEffect(() => {
setTimeout(() => {
window.location.href = '/'
}, 3000)
})
}
return <div
className='text-black bg-white h-screen text-center justify-center content-center items-center flex flex-col'>
<div>
<h1 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'>404</h1>
<div className='inline-block text-left h-32 leading-10 align-middle'>
<h2 className='m-0 p-0'>页面丢失了3秒后返回首页</h2></div>
</div>
</div>
return <Layout404 />
}

View File

@@ -1,26 +1,31 @@
import '@/styles/notion.css'
import 'rc-dropdown/assets/index.css'
import 'katex/dist/katex.min.css'
import '@/styles/globals.css'
import 'prismjs'
import 'prismjs/themes/prism-okaidia.css'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-bash'
import BLOG from 'blog.config'
import 'animate.css'
import 'font-awesome/css/font-awesome.min.css'
import BLOG from '@/blog.config'
import '@/styles/globals.css'
// custom
// core styles shared by all of react-notion-x (required)
import 'react-notion-x/src/styles.css'
import '@/styles/notion.css' // 重写部分样式
// used for collection views (optional)
import 'rc-dropdown/assets/index.css'
// used for code syntax highlighting (optional)
import 'prismjs/themes/prism-okaidia.css'
// used for rendering equations (optional)
import 'katex/dist/katex.min.css'
import dynamic from 'next/dynamic'
import { LocaleProvider } from '@/lib/locale'
import { ThemeProvider } from '@/lib/theme'
import { GlobalContextProvider } from '@/lib/global'
import { config } from '@fortawesome/fontawesome-svg-core'
import '@fortawesome/fontawesome-svg-core/styles.css'
config.autoAddCss = false
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
function MyApp ({ Component, pageProps }) {
const MyApp = ({ Component, pageProps }) => {
return (
<LocaleProvider>
<ThemeProvider>
<GlobalContextProvider>
{BLOG.isProd && BLOG?.analytics?.provider === 'ackee' && (
<Ackee
ackeeServerUrl={BLOG.analytics.ackeeConfig.dataAckeeServer}
@@ -28,9 +33,10 @@ function MyApp ({ Component, pageProps }) {
/>
)}
{BLOG.isProd && BLOG?.analytics?.provider === 'ga' && <Gtag />}
{BLOG.analytics.busuanzi && <Busuanzi/>}
{BLOG.googleAdsenseId && <GoogleAdsense/>}
<Component {...pageProps} />
</ThemeProvider>
</LocaleProvider>
</GlobalContextProvider>
)
}

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line @next/next/no-document-import-in-page
import Document, { Html, Head, Main, NextScript } from 'next/document'
import BLOG from '@/blog.config'
import ThirdPartyScript from '@/components/ThirdPartyScript'
import CommonScript from '@/components/CommonScript'
class MyDocument extends Document {
static async getInitialProps (ctx) {
@@ -14,10 +15,10 @@ class MyDocument extends Document {
<Head>
<link rel='icon' href='/favicon.ico' />
<link rel='icon' href='/favicon.svg' type='image/svg+xml' />
<ThirdPartyScript />
<CommonScript />
</Head>
<body>
<body className={`${BLOG.font} bg-day dark:bg-night duration-200`}>
<Main />
<NextScript />
</body>

60
pages/about.js Normal file
View File

@@ -0,0 +1,60 @@
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import Custom404 from '@/pages/404'
import React from 'react'
import { LayoutSlug } from '@/themes'
/**
* 关于页面默认取notion中slug为about的文章
* @param {*} props
* @returns
*/
const About = (props) => {
if (!props.post) {
return <Custom404 />
}
return <LayoutSlug {...props} />
}
export async function getStaticProps () {
const from = 'about-props'
const {
allPosts,
categories,
tags,
postCount,
latestPosts
} = await getGlobalNotionData({
from,
includePage: true
})
const post = allPosts.find(p => p.slug === 'about')
if (!post) {
return {
props: {},
revalidate: 1
}
}
post.blockMap = await getPostBlocks(post.id, 'slug')
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,
tags,
prev,
next,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
export default About

24
pages/archive/index.js Normal file
View File

@@ -0,0 +1,24 @@
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React from 'react'
import { LayoutArchive } from '@/themes'
export async function getStaticProps () {
const { allPosts, categories, tags, postCount } =
await getGlobalNotionData({ from: 'archive-index' })
return {
props: {
posts: allPosts,
tags,
categories,
postCount
},
revalidate: 1
}
}
const ArchiveIndex = (props) => {
return <LayoutArchive {...props}/>
}
export default ArchiveIndex

View File

@@ -1,60 +1,112 @@
import ArticleLayout from '@/layouts/ArticleLayout'
import { getAllPosts, getAllTags, getPostBlocks } from '@/lib/notion'
import BLOG from '@/blog.config'
import { createHash } from 'crypto'
import { getPageTableOfContents } from 'notion-utils'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { LayoutSlug } from '@/themes'
import Custom404 from '@/pages/404'
const BlogPost = ({ post, blockMap, emailHash, tags, prev, next }) => {
if (!post) {
return <Custom404/>
/**
* 根据notion的slug访问页面
* @param {*} props
* @returns
*/
const Slug = (props) => {
if (!props.post) {
return <Custom404 />
}
return (
<ArticleLayout
blockMap={blockMap}
frontMatter={post}
emailHash={emailHash}
tags={tags}
prev={prev}
next={next}
></ArticleLayout>
)
return <LayoutSlug {...props}/>
}
export async function getStaticPaths () {
let posts = await getAllPosts()
posts = posts.filter(post => post.status[0] === 'Published')
if (!BLOG.isProd) {
return {
paths: [],
fallback: true
}
}
const from = '[slug-paths'
const { allPosts } = await getGlobalNotionData({ from, includePage: false })
return {
paths: posts.map(row => `${BLOG.path}/article/${row.slug}`),
paths: allPosts.map(row => ({ params: { slug: row.slug } })),
fallback: true
}
}
export async function getStaticProps ({ params: { slug } }) {
let posts = await getAllPosts()
posts = posts.filter(post => post.status[0] === 'Published')
const post = posts.find(t => t.slug === slug)
const from = `slug-props-${slug}`
const { allPosts, categories, tags, postCount, latestPosts } =
await getGlobalNotionData({ from, includePage: false })
const post = allPosts.find(p => p.slug === slug)
if (!post) {
return {
props: { },
revalidate: 1
}
return { props: {}, revalidate: 1 }
}
const blockMap = await getPostBlocks(post.id)
const emailHash = createHash('md5').update(BLOG.email).digest('hex')
post.toc = getPageTableOfContents(post, blockMap)
posts = posts.filter(post => post.type[0] === 'Post')
const tags = await getAllTags(posts)
// 获取推荐文章
const index = posts.indexOf(post)
const prev = posts.slice(index - 1, index)[0] ?? posts.slice(-1)[0]
const next = posts.slice(index + 1, index + 2)[0] ?? posts[0]
post.blockMap = await getPostBlocks(post.id, 'slug')
// 上一篇、下一篇文章关联
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]
const recommendPosts = getRecommendPost(post, allPosts)
return {
props: { post, blockMap, emailHash, tags, prev, next },
props: {
post,
tags,
prev,
next,
recommendPosts,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
export default BlogPost
/**
*
* @param post
* @param {*} allPosts
* @param {*} count
* @returns
*/
function getRecommendPost (post, allPosts, count = 5) {
let filteredPosts = Object.create(allPosts)
// 筛选同标签
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
)
}
shuffleSort(filteredPosts)
// 筛选前5个
if (filteredPosts.length > count) {
filteredPosts = filteredPosts.slice(0, count)
}
return filteredPosts
}
/**
* 洗牌乱序:从数组的最后位置开始,从前面随机一个位置,对两个数进行交换,直到循环完毕
* @param arr
* @returns {*}
*/
function shuffleSort (arr) {
let i = arr.length - 1
while (i > 0) {
const rIndex = Math.floor(Math.random() * i)
const temp = arr[rIndex]
arr[rIndex] = arr[i]
arr[i] = temp
i--
}
return arr
}
export default Slug

View File

@@ -0,0 +1,42 @@
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React from 'react'
import { LayoutCategory } from '@/themes'
export default function Category (props) {
return <LayoutCategory {...props} />
}
export async function getStaticProps ({ params }) {
const from = 'category-props'
const category = params.category
const {
allPosts,
categories,
tags,
postCount,
latestPosts
} = await getGlobalNotionData({ from })
const filteredPosts = allPosts.filter(
post => post && post.category && post.category.includes(category)
)
return {
props: {
tags,
posts: filteredPosts,
category,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
export async function getStaticPaths () {
const from = 'category-paths'
const { categories } = await getGlobalNotionData({ from })
return {
paths: Object.keys(categories).map(category => ({ params: { category } })),
fallback: true
}
}

23
pages/category/index.js Normal file
View File

@@ -0,0 +1,23 @@
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React from 'react'
import { LayoutCategoryIndex } from '@/themes'
export default function Category (props) {
return <LayoutCategoryIndex {...props}/>
}
export async function getStaticProps () {
const from = 'category-index-props'
const { allPosts, categories, tags, postCount, latestPosts } = await getGlobalNotionData({ from })
return {
props: {
tags,
allPosts,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}

View File

@@ -3,7 +3,7 @@ import { generateRss } from '@/lib/rss'
export async function getServerSideProps ({ res }) {
res.setHeader('Content-Type', 'text/xml')
let posts = await getAllPosts()
let posts = await getAllPosts({ from: 'feed' })
posts = posts
.filter(post => post.status[0] === 'Published' && post.type[0] === 'Post')
.slice(0, 10)

View File

@@ -1,27 +1,51 @@
import { getAllPosts, getAllTags } from '@/lib/notion'
import DefaultLayout from '@/layouts/DefaultLayout'
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { LayoutIndex } from '@/themes'
const Index = (props) => {
return <LayoutIndex {...props}/>
}
export async function getStaticProps () {
let posts = await getAllPosts()
posts = posts.filter(
post => post.status[0] === 'Published' && post.type[0] === 'Post'
)
const tags = await getAllTags(posts)
const from = 'index'
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', BLOG.home.previewLines)
if (blockMap) {
post.blockMap = blockMap
}
}
}
return {
props: {
page: 1, // current page is 1
posts,
tags
posts: postsToShow,
latestPosts,
postCount,
tags,
categories,
meta
},
revalidate: 1
}
}
const blog = ({ posts, page, tags }) => {
return (
<DefaultLayout tags={tags} posts={posts} page={page} />
)
}
export default blog
export default Index

View File

@@ -1,63 +1,67 @@
import { getAllPosts, getAllTags } from '@/lib/notion'
import BLOG from '@/blog.config'
import DefaultLayout from '@/layouts/DefaultLayout'
import { useRouter } from 'next/router'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { LayoutPage } from '@/themes'
import Custom404 from '@/pages/404'
const Page = ({ posts, tags, page }) => {
let filteredBlogPosts = posts
if (posts) {
const router = useRouter()
if (router.query && router.query.s) {
filteredBlogPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(router.query.s.toLowerCase())
})
const Page = (props) => {
if (!props?.meta) {
return <Custom404 />
}
return <LayoutPage {...props} />
}
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', BLOG.home.previewLines)
if (blockMap) {
post.blockMap = blockMap
}
}
return <DefaultLayout tags={tags} posts={filteredBlogPosts} page={page} />
}
export async function getStaticProps (context) {
const { page } = context.params // Get Current Page No.
let posts = await getAllPosts()
posts = posts.filter(
post => post.status[0] === 'Published' && post.type[0] === 'Post'
)
const tags = await getAllTags(posts)
return {
props: {
page,
posts: postsToShow,
postCount,
latestPosts,
tags,
posts,
page
categories,
meta
},
revalidate: 1
}
}
export async function getStaticPaths () {
if (BLOG.isProd) {
// 预渲染
let posts = await getAllPosts()
posts = posts.filter(
post => post.status[0] === 'Published' && post.type[0] === 'Post'
)
const totalPosts = posts.length
const totalPages = Math.ceil(totalPosts / 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
}
} else {
return {
paths: [],
fallback: true
}
}
}
export default Page

28
pages/search.js Normal file
View File

@@ -0,0 +1,28 @@
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { LayoutSearch } from '@/themes'
export async function getStaticProps () {
const {
allPosts,
categories,
tags,
postCount,
latestPosts
} = await getGlobalNotionData({ from: 'search-props' })
return {
props: {
posts: allPosts,
tags,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
const Search = (props) => {
return <LayoutSearch {...props} />
}
export default Search

View File

@@ -1,43 +1,65 @@
import { getAllPosts, getAllTags } from '@/lib/notion'
import DefaultLayout from '@/layouts/DefaultLayout'
import BLOG from '@/blog.config'
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { LayoutTag } from '@/themes'
export default function Tag ({ tags, posts, currentTag }) {
return <DefaultLayout tags={tags} posts={posts} currentTag={currentTag} />
const Tag = (props) => {
return <LayoutTag {...props} />
}
export async function getStaticProps ({ params }) {
const currentTag = params.tag
let posts = await getAllPosts()
posts = posts.filter(
post => post.status[0] === 'Published' && post.type[0] === 'Post'
)
const tags = await getAllTags(posts)
const filteredPosts = posts.filter(
post => post && post.tags && post.tags.includes(currentTag)
const tag = params.tag
const from = 'tag-props'
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,
posts: filteredPosts,
currentTag
tag,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
/**
* 获取所有的标签
* @returns
* @param tags
*/
function getTagNames (tags) {
const tagNames = []
tags.forEach(tag => {
tagNames.push(tag.name)
})
return tagNames
}
export async function getStaticPaths () {
if (BLOG.isProd) {
// 预渲染
const tags = await getAllTags()
return {
paths: Object.keys(tags).map(tag => ({ params: { tag } })),
fallback: true
}
} else {
return {
paths: [],
fallback: true
}
const from = 'tag-static-path'
const { tags } = await getGlobalNotionData({
from,
tagsCount: 0
})
const tagNames = getTagNames(tags)
return {
paths: Object.keys(tagNames).map(index => ({ params: { tag: tagNames[index] } })),
fallback: true
}
}
export default Tag

33
pages/tag/index.js Normal file
View File

@@ -0,0 +1,33 @@
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import React from 'react'
import { LayoutTagIndex } from '@/themes'
const TagIndex = (props) => {
return <LayoutTagIndex {...props} />
}
export async function getStaticProps () {
const from = 'tag-index-props'
const {
categories,
tags,
postCount,
latestPosts
} = await getGlobalNotionData({
from,
includePage: true,
tagsCount: 0
})
return {
props: {
tags,
categories,
postCount,
latestPosts
},
revalidate: 1
}
}
export default TagIndex

BIN
public/avatar.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,898 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> <image id="image0" width="256" height="256" x="0" y="0"
href="data:image/png;base64,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
YII=" />
</svg>

Before

Width:  |  Height:  |  Size: 68 KiB

BIN
public/bg_image.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
public/reward_code_wechat.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -98,7 +98,7 @@ nav {
}
.shadow-card{
box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 1px 3px 0 rgba(0,0,0,.12);
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px, rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px, rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.gt-meta{
@@ -109,12 +109,83 @@ nav {
@apply right-auto left-0 hidden lg:block z-10 !important
}
@media (max-width: 1300px){
.sidebar{
-ms-overflow-style: none;
overflow: -moz-scrollbars-none;
@apply border-r border-gray-200 h-screen overflow-y-scroll fixed left-0
}
.sidebar::-webkit-scrollbar { width: 0 !important }
/* 隐藏滚动条 */
.scroll-hidden{
-ms-overflow-style: none;
overflow: -moz-scrollbars-none;
}
.scroll-hidden::-webkit-scrollbar { width: 0 !important }
.notion-collection{
@apply max-w-0
}
/* 渐变透明度 */
.line-x-opacity{
background: -webkit-linear-gradient(left, rgba(255,255,255,0), rgb(255, 255, 255)); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(right, rgba(255, 255, 255,0), rgb(255, 255, 255)); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(right, rgba(255, 255, 255,0), rgba(255, 255, 255)); /* Firefox 3.6 - 15 */
background: linear-gradient(to right, rgba(255, 255, 255,0), rgba(255, 255, 255)); /* 标准的语法(必须放在最后) */
}
.-line-x-opacity{
background: -webkit-linear-gradient(right, rgba(255,255,255,0), rgb(255, 255, 255)); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(left, rgba(255, 255, 255,0), rgb(255, 255, 255)); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(left, rgba(255, 255, 255,0), rgba(255, 255, 255)); /* Firefox 3.6 - 15 */
background: linear-gradient(to left, rgba(255, 255, 255,0), rgba(255, 255, 255)); /* 标准的语法(必须放在最后) */
}
.dark .line-x-opacity{
background: -webkit-linear-gradient(left, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(right, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(right, rgba(31, 41, 55,0), rgba(31, 41, 55)); /* Firefox 3.6 - 15 */
background: linear-gradient(to right, rgba(31, 41, 55,0), rgba(31, 41, 55)); /* 标准的语法(必须放在最后) */
}
.dark .-line-x-opacity{
background: -webkit-linear-gradient(right, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(left, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(left, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* Firefox 3.6 - 15 */
background: linear-gradient(to left, rgba(31, 41, 55,0), rgb(31, 41, 55)); /* 标准的语法(必须放在最后) */
}
.glassmorphism{
background: hsla(0, 0%, 100%, .75);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.dark .glassmorphism{
background: rgba(31, 41, 55, .75);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.medium-zoom-overlay{
background: none !important;
}
.article-cover{
-webkit-text-size-adjust: 100%;
font-size: 14px;
font-family: -apple-system,SF UI Text,Arial,PingFang SC,Hiragino Sans GB,Microsoft YaHei,WenQuanYi Micro Hei,sans-serif;
-webkit-font-smoothing: antialiased;
color: rgba(0,0,0,.75);
font-variant-ligatures: common-ligatures;
line-height: 1.625;
tab-size: 4;
outline: 0;
font-weight: normal;
-webkit-box-sizing: border-box;
padding: 0;
margin: 0;
position: relative;
z-index: 998;
padding-top: 160px;
bottom: -1px;
margin-top: -200px;
text-align: center;
width: 100%;
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,#fff 70%);
padding-bottom: 34px;
}

View File

@@ -37,6 +37,8 @@
--notion-orange_background: rgb(250, 235, 221);
--notion-brown_background: rgb(233, 229, 227);
--notion-gray_background: rgb(235, 236, 237);
--notion-green_background: rgb(219, 237, 219);
--notion-default_background: rgba(227, 226, 224);
--notion-red_background_co: rgba(251, 228, 228, 0.3);
--notion-pink_background_co: rgba(244, 223, 235, 0.3);
@@ -47,6 +49,8 @@
--notion-orange_background_co: rgba(250, 235, 221, 0.3);
--notion-brown_background_co: rgba(233, 229, 227, 0.3);
--notion-gray_background_co: rgba(235, 236, 237, 0.3);
--notion-green_background_co: rgba(219, 237, 219, 0.3);
--notion-default_background_co: rgba(227, 226, 224, 0.3);
--notion-item-blue: rgba(0, 120, 223, 0.2);
--notion-item-orange: rgba(245, 93, 0, 0.2);
@@ -64,10 +68,11 @@
}
.notion {
font-size: 16px;
font-size: 17px;
line-height: 1.5;
color: var(--fg-color);
caret-color: var(--fg-color);
font-family: inherit;
}
.notion > * {
@@ -186,10 +191,14 @@
left: 0;
right: 0;
bottom: 0;
z-index: -10;
pointer-events: none;
}
.medium-zoom-overlay {
z-index: 300;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
}
.medium-zoom-image {
@@ -279,6 +288,13 @@
.notion-gray_background {
background-color: var(--notion-gray_background);
}
.notion-green_background {
background-color: var(--notion-green_background);
}
.notion-default_background {
background-color: var(--notion-default_background);
}
.notion-red_background_co {
background-color: var(--notion-red_background_co);
}
@@ -306,6 +322,12 @@
.notion-gray_background_co {
background-color: var(--notion-gray_background_co);
}
.notion-green_background_co {
background-color: var(--notion-green_background_co);
}
.notion-default_background_co {
background-color: var(--notion-default_background_co);
}
.notion-item-blue {
background-color: var(--notion-item-blue);
@@ -365,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 {
@@ -603,6 +626,7 @@ svg.notion-page-icon {
margin: 0;
margin-block-start: 0.6em;
margin-block-end: 0.6em;
@apply w-full;
}
.notion-list-disc {
@@ -627,7 +651,7 @@ svg.notion-page-icon {
}
.notion-list li {
padding: 6px 0;
padding: 1px 0;
white-space: pre-wrap;
}
@@ -689,12 +713,12 @@ svg.notion-page-icon {
width: 100%;
padding: 30px 16px 30px 20px;
margin: 4px 0;
border-radius: 3px;
border-radius: 10px;
tab-size: 2;
display: block;
box-sizing: border-box;
overflow: auto;
background: var(--bg-color-1);
background: #272822;
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
}
@@ -1059,7 +1083,7 @@ svg.notion-page-icon {
padding: 6px 2px;
font-size: 14px;
line-height: 1.3;
line-height: 1.2;
display: flex;
align-items: center;
@@ -1095,7 +1119,7 @@ svg.notion-page-icon {
padding-left: 1.5em;
}
.notion-to-do-checked .notion-to-do-item {
.notion-to-do-checked {
text-decoration: line-through;
opacity: 0.375;
}
@@ -1107,6 +1131,11 @@ svg.notion-page-icon {
.notion-to-do-item .notion-property-checkbox {
margin-right: 8px;
/* @apply w-4 h-4 border-2 */
}
.notion-property-checkbox-checked {
/* @apply bg-white */
}
.notion-google-drive {
@@ -1556,7 +1585,7 @@ svg.notion-page-icon {
/* NOTION CSS OVERRIDE */
.notion {
@apply text-gray-900 dark:text-gray-300;
@apply dark:text-gray-300;
overflow-wrap: break-word;
}
.notion,
@@ -1613,9 +1642,6 @@ pre[class*='language-'] {
.notion-bookmark:hover {
@apply border-blue-400;
}
.notion-viewport {
z-index: -10;
}
.notion-asset-caption {
@apply text-center;
}

View File

@@ -7,11 +7,22 @@ const fontSansCJK = !CJK()
const fontSerifCJK = !CJK()
? []
: [`"Noto Serif CJK ${CJK()}"`, `"Noto Serif ${CJK()}"`]
module.exports = {
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js'],
purge: ['./pages/**/*.js', './components/**/*.js', './layouts/**/*.js', './themes/**/*.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: {
@@ -20,18 +31,6 @@ module.exports = {
night: {
DEFAULT: BLOG.darkBackground || '#111827'
}
},
fontFamily: {
sans: ['"IBM Plex Sans"', ...fontSansCJK, ...fontFamily.sans],
serif: ['"Source Serif"', ...fontSerifCJK, ...fontFamily.serif],
noEmoji: [
'"IBM Plex Sans"',
'ui-sans-serif',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'sans-serif'
]
}
}
},

35
themes/NEXT/Layout404.js Normal file
View File

@@ -0,0 +1,35 @@
import { useRouter } from 'next/router'
import LayoutBase from './LayoutBase'
import BLOG from '@/blog.config'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { useEffect } from 'react'
export const Layout404 = () => {
const router = useRouter()
useEffect(() => {
// 延时3秒如果加载失败就返回首页
setTimeout(() => {
if (window) {
const article = document.getElementById('container')
if (!article) {
router.push('/').then(() => {
console.log('找不到页面', router.asPath)
})
}
}
}, 30000000)
})
return <LayoutBase meta={{ title: `${BLOG.title} | 页面找不到啦` }}>
<div
className='md:-mt-20 text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
<div className='dark:text-gray-200'>
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'><FontAwesomeIcon icon={faSpinner} spin={true} className='mr-2'/>404</h2>
<div className='inline-block text-left h-32 leading-10 items-center'>
<h2 className='m-0 p-0'>页面无法加载即将返回首页</h2>
</div>
</div>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,65 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import React, { useEffect } from 'react'
import LayoutBase from './LayoutBase'
import BlogPostArchive from './components/BlogPostArchive'
import Live2D from './components/Live2D'
export const LayoutArchive = ({ posts, tags, categories, postCount }) => {
const { locale } = useGlobal()
// 深拷贝
const postsSortByDate = Object.create(posts)
// 时间排序
postsSortByDate.sort((a, b) => {
const dateA = new Date(a?.date.start_date || a.createdTime)
const dateB = new Date(b?.date.start_date || b.createdTime)
return dateB - dateA
})
const meta = {
title: `${locale.NAV.ARCHIVE} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
const archivePosts = {}
postsSortByDate.forEach(post => {
const date = post.date.start_date.slice(0, 7)
if (archivePosts[date]) {
archivePosts[date].push(post)
} else {
archivePosts[date] = [post]
}
})
useEffect(() => {
if (window) {
const anchor = window.location.hash
if (anchor) {
setTimeout(() => {
const anchorElement = document.getElementById(anchor.substring(1))
if (anchorElement) {
anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
}, 300)
}
}
}, [])
return (
<LayoutBase 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 />
</LayoutBase>
)
}

112
themes/NEXT/LayoutBase.js Normal file
View File

@@ -0,0 +1,112 @@
import BLOG from '@/blog.config'
import CommonHead from '@/components/CommonHead'
import FloatDarkModeButton from './components/FloatDarkModeButton'
import Footer from './components/Footer'
import JumpToBottomButton from './components/JumpToBottomButton'
import JumpToTopButton from './components/JumpToTopButton'
import LoadingCover from './components/LoadingCover'
import SideAreaLeft from './components/SideAreaLeft'
import SideAreaRight from './components/SideAreaRight'
import TopNav from './components/TopNav'
import { useGlobal } from '@/lib/global'
import PropTypes from 'prop-types'
import React, { useEffect, useRef, useState } from 'react'
import smoothscroll from 'smoothscroll-polyfill'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
* @param children
* @param layout
* @param tags
* @param meta
* @param post
* @param currentSearch
* @param currentCategory
* @param currentTag
* @param categories
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = ({
children,
headerSlot,
tags,
meta,
post,
postCount,
sideBarSlot,
floatSlot,
rightAreaSlot,
currentSearch,
currentCategory,
currentTag,
categories
}) => {
const { onLoading } = useGlobal()
const targetRef = useRef(null)
const [show, switchShow] = useState(false)
const [percent, changePercent] = useState(0) // 页面阅读百分比
const scrollListener = () => {
const targetRef = document.getElementById('wrapper')
const clientHeight = targetRef?.clientHeight
const scrollY = window.pageYOffset
const fullHeight = clientHeight - window.outerHeight
let per = parseFloat(((scrollY / fullHeight * 100)).toFixed(0))
if (per > 100) per = 100
const shouldShow = scrollY > 100 && per > 0
if (shouldShow !== show) {
switchShow(shouldShow)
}
changePercent(per)
}
useEffect(() => {
smoothscroll.polyfill()
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<>
<CommonHead meta={meta} />
<TopNav tags={tags} postCount={postCount} post={post} slot={sideBarSlot} currentSearch={currentSearch} categories={categories} currentCategory={currentCategory} />
<>{headerSlot}</>
<div className='h-0.5 w-full bg-gray-700 dark:bg-gray-600 hidden lg:block'/>
<main id='wrapper' className='flex justify-center flex-1 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' : ''} lg:max-w-3xl xl:max-w-4xl flex-grow md:mt-0 min-h-screen w-full`} ref={targetRef}>
{onLoading
? <LoadingCover/>
: <>
{children}
</>
}
</section>
<SideAreaRight targetRef={targetRef} post={post} slot={rightAreaSlot} postCount={postCount} tags={tags} currentSearch={currentSearch} currentTag={currentTag} categories={categories} currentCategory={currentCategory}/>
</main>
{/* 右下角悬浮 */}
<div className='right-8 bottom-10 lg:right-2 lg:bottom-2 fixed justify-end z-20 font-sans'>
<div className={(show ? 'animate__animated ' : 'hidden') + ' animate__fadeInUp rounded-md glassmorphism justify-center duration-500 animate__faster flex space-x-2 items-center cursor-pointer '}>
<JumpToTopButton percent={percent}/>
<JumpToBottomButton />
<FloatDarkModeButton/>
{floatSlot}
</div>
</div>
<Footer title={meta.title}/>
</>
)
}
LayoutBase.propTypes = {
children: PropTypes.node
}
export default LayoutBase

View File

@@ -0,0 +1,23 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import LayoutBase from '@/themes/NEXT/LayoutBase'
import StickyBar from './components/StickyBar'
import CategoryList from './components/CategoryList'
import BlogPostListScroll from './components/BlogPostListScroll'
export const LayoutCategory = ({ tags, posts, category, categories, latestPosts, postCount }) => {
const { locale } = useGlobal()
const meta = {
title: `${category} | ${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <LayoutBase 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={posts} tags={tags} currentCategory={category}/>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,38 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import LayoutBase from './LayoutBase'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolder, faThList } from '@fortawesome/free-solid-svg-icons'
import Link from 'next/link'
export const LayoutCategoryIndex = ({
tags,
allPosts,
categories,
postCount,
latestPosts
}) => {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.CATEGORY} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <LayoutBase 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'>
{Object.keys(categories).map(category => {
return <Link key={category} href={`/category/${category}`} passHref>
<div
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
<FontAwesomeIcon icon={faFolder} className='mr-4' />{category}({categories[category]})
</div>
</Link>
})}
</div>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,29 @@
import LayoutBase from './LayoutBase'
import Header from './components/Header'
import LatestPostsGroup from './components/LatestPostsGroup'
import Card from './components/Card'
import BlogPostListScroll from './components/BlogPostListScroll'
import BlogPostListPage from './components/BlogPostListPage'
import { CONFIG_NEXT } from './index'
export const LayoutIndex = ({ posts, tags, meta, categories, postCount, latestPosts }) => {
return <LayoutBase
headerSlot={CONFIG_NEXT.HOME_BANNER && <Header />}
meta={meta}
tags={tags}
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
rightAreaSlot={
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
}
postCount={postCount}
categories={categories}
>
{CONFIG_NEXT.POSTS_LIST_TYPE !== 'page'
? (
<BlogPostListScroll posts={posts} tags={tags} showSummary={true} />
)
: (
<BlogPostListPage posts={posts} tags={tags} postCount={postCount} />
)}
</LayoutBase>
}

19
themes/NEXT/LayoutPage.js Normal file
View File

@@ -0,0 +1,19 @@
import LayoutBase from './LayoutBase'
import LatestPostsGroup from './components/LatestPostsGroup'
import BlogPostListPage from './components/BlogPostListPage'
import { CONFIG_NEXT } from './index'
export const LayoutPage = ({ page, posts, tags, meta, categories, postCount, latestPosts }) => {
return (
<LayoutBase
meta={meta}
tags={tags}
sideBarSlot={<LatestPostsGroup posts={latestPosts} />}
rightAreaSlot={CONFIG_NEXT.RIGHT_LATEST_POSTS && <LatestPostsGroup posts={latestPosts} />}
postCount={postCount}
categories={categories}
>
<BlogPostListPage page={page} posts={posts} postCount={postCount} />
</LayoutBase>
)
}

View File

@@ -0,0 +1,56 @@
import LayoutBase from './LayoutBase'
import StickyBar from './components/StickyBar'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import BlogPostListScroll from './components/BlogPostListScroll'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
export const LayoutSearch = ({ posts, tags, categories, postCount }) => {
let filteredPosts
const searchKey = getSearchKey()
if (searchKey) {
filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : ''
const searchContent = post.title + post.summary + tagContent
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
})
} else {
filteredPosts = posts
}
const { locale } = useGlobal()
const meta = {
title: `${searchKey || ''} | ${locale.NAV.SEARCH} | ${BLOG.title} `,
description: BLOG.description,
type: 'website'
}
return (
<LayoutBase
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>
</LayoutBase>
)
}
function getSearchKey () {
const router = useRouter()
if (router.query && router.query.s) {
return router.query.s
}
return null
}

76
themes/NEXT/LayoutSlug.js Normal file
View File

@@ -0,0 +1,76 @@
import BLOG from '@/blog.config'
import { getPageTableOfContents } from 'notion-utils'
import TocDrawerButton from './components/TocDrawerButton'
import LayoutBase from './LayoutBase'
import Card from './components/Card'
import LatestPostsGroup from './components/LatestPostsGroup'
import ArticleDetail from './components/ArticleDetail'
import TocDrawer from './components/TocDrawer'
import Live2D from './components/Live2D'
import { useRef } from 'react'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { CONFIG_NEXT } from './index'
export const LayoutSlug = ({
post,
tags,
prev,
next,
recommendPosts,
categories,
postCount,
latestPosts
}) => {
const meta = {
title: `${post.title} | ${BLOG.title}`,
description: post.summary,
type: 'article',
tags: post.tags
}
const drawerRight = useRef(null)
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
post.content = Object.keys(post?.blockMap?.block)
post.toc = getPageTableOfContents(post, post.blockMap)
const floatSlot = post?.toc?.length > 1
? <div className='block lg:hidden'><TocDrawerButton onClick={() => {
drawerRight?.current?.handleSwitchVisible()
}} /></div>
: null
return (
<LayoutBase
meta={meta}
tags={tags}
post={post}
postCount={postCount}
latestPosts={latestPosts}
categories={categories}
floatSlot={floatSlot}
rightAreaSlot={
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
}
>
<ArticleDetail
post={post}
recommendPosts={recommendPosts}
prev={prev}
next={next}
/>
{/* 悬浮目录按钮 */}
<div className='block lg:hidden'>
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
</div>
{/* 宠物 */}
<Live2D />
</LayoutBase>
)
}

30
themes/NEXT/LayoutTag.js Normal file
View File

@@ -0,0 +1,30 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import LayoutBase from './LayoutBase'
import StickyBar from './components/StickyBar'
import TagList from './components/TagList'
import BlogPostListScroll from './components/BlogPostListScroll'
export const LayoutTag = ({ tags, posts, tag, categories, postCount, latestPosts }) => {
const { locale } = useGlobal()
const meta = {
title: `${tag} | ${locale.COMMON.TAGS} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
// 将当前选中的标签置顶🔝
if (!tags) tags = []
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 <LayoutBase 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={posts} tags={tags} currentTag={tag}/>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,25 @@
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import LayoutBase from './LayoutBase'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTags } from '@fortawesome/free-solid-svg-icons'
import TagItem from './components/TagItem'
export const LayoutTagIndex = ({ tags, categories, postCount, latestPosts }) => {
const { locale } = useGlobal()
const meta = {
title: `${locale.COMMON.TAGS} | ${BLOG.title}`,
description: BLOG.description,
type: 'website'
}
return <LayoutBase meta={meta} categories={categories} 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'>
{ tags.map(tag => {
return <div key={tag.name} className='p-2'><TagItem key={tag.name} tag={tag} /></div>
}) }
</div>
</div>
</LayoutBase>
}

View File

@@ -0,0 +1,30 @@
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
export default function ArticleCopyright ({ author, url }) {
if (!BLOG.widget?.showCopyRight) {
return <></>
}
const { locale } = useGlobal()
return <section className="dark:text-gray-300 mt-6">
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-700 bg-gray-100 p-5 leading-8 border-l-2 border-blue-500">
<li>
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
<Link href="/about">
<a className="hover:underline">{author}</a>
</Link>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.URL}:</strong>
<a className="hover:underline" href={url}>
{url}
</a>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
{locale.COMMON.COPYRIGHT_NOTICE}
</li>
</ul>
</section>
}

View File

@@ -0,0 +1,201 @@
import BLOG from '@/blog.config'
import BlogAround from '@/themes/NEXT/components/BlogAround'
import Comment from '@/components/Comment'
import RecommendPosts from '@/themes/NEXT/components/RecommendPosts'
import ShareBar from '@/themes/NEXT/components/ShareBar'
import TagItem from '@/themes/NEXT/components/TagItem'
import formatDate from '@/lib/formatDate'
import { useGlobal } from '@/lib/global'
import { faEye, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import mediumZoom from 'medium-zoom'
import Link from 'next/link'
import { useRouter } from 'next/router'
import 'prismjs'
import 'prismjs/components/prism-bash'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-markup'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-typescript'
import { useEffect, useRef } from 'react'
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
import ArticleCopyright from './ArticleCopyright'
import WordCount from './WordCount'
/**
*
* @param {*} param0
* @returns
*/
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
const url = BLOG.link + useRouter().asPath
const { locale } = useGlobal()
const date = formatDate(post?.date?.start_date || post.createdTime, locale.LOCALE)
const zoom = typeof window !== 'undefined' && mediumZoom({
container: '.notion-viewport',
background: 'rgba(0, 0, 0, 0.2)',
margin: getMediumZoomMargin()
})
const zoomRef = useRef(zoom ? zoom.clone() : null)
useEffect(() => {
// 将所有container下的所有图片添加medium-zoom
const container = document.getElementById('container')
const imgList = container.getElementsByTagName('img')
if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
(zoomRef.current).attach(imgList[i])
}
}
})
return (<div id="container" className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
<article itemScope itemType="https://schema.org/Movie"
className="subpixel-antialiased py-10 px-5 lg:pt-24 md:px-24 dark:border-gray-700 bg-white dark:bg-gray-800"
>
<header className='animate__slideInDown animate__animated'>
{post.type && !post.type.includes('Page') && post?.page_cover && (
<div className="w-full relative md:flex-shrink-0 overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img alt={post.title} src={post?.page_cover} className='object-center w-full' />
{/* <div className="w-full h-60 relative lg:h-96 transform duration-200 md:flex-shrink-0 overflow-hidden">
<Image
src={post?.page_cover}
loading="eager"
objectFit="cover"
layout="fill"
alt={post.title}
/>
</div> */}
</div>
)}
{/* 文章Title */}
<div className="font-bold text-3xl text-black dark:text-white font-serif pt-10">
{post.title}
</div>
<section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
<div>
<Link href={`/category/${post.category}`} passHref>
<a className="cursor-pointer text-md mr-2 hover:text-black dark:hover:text-white border-b dark:border-gray-500 border-dashed">
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
{post.category}
</a>
</Link>
<span className='mr-2'>|</span>
{post.type[0] !== 'Page' && (<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
>
<a className="pl-1 mr-2 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 border-b dark:border-gray-500 border-dashed">
{date}
</a>
</Link>
<span className='mr-2'>|</span>
</>)}
<div className="hidden busuanzi_container_page_pv font-light mr-2">
<FontAwesomeIcon icon={faEye} className='mr-1'/>
&nbsp;
<span className="mr-2 busuanzi_value_page_pv"
></span>
<span className='mr-2'>|</span>
</div>
</div>
<div className='flex flex-nowrap whitespace-nowrap items-center font-light text-md'>
<WordCount/>
</div>
</section>
{/* <hr className="mt-2" /> */}
</header>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
components={{
equation: Equation,
code: Code,
collectionRow: CollectionRow,
collection: Collection
}}
/>
)}
</section>
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
{/* 文章内嵌广告 */}
<ins className="adsbygoogle"
style={{ display: 'block', textAlign: 'center' }}
data-adtest="on"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-2708419466378217"
data-ad-slot="3806269138"></ins>
</section>
{/* 版权声明 */}
<ArticleCopyright author={BLOG.author} url={url} />
{/* 推荐文章 */}
<RecommendPosts currentPost={post} recommendPosts={recommendPosts} />
{/* 标签列表 */}
<section className="md:flex md:justify-between">
{post.tagItems && (
<div className="flex flex-nowrap leading-8 p-1 py-4 overflow-x-auto">
<div className="hidden md:block dark:text-gray-300 whitespace-nowrap">
{locale.COMMON.TAGS}
</div>
{post.tagItems.map(tag => (
<TagItem key={tag.name} tag={tag} />
))}
</div>
)}
<div>
<ShareBar post={post} />
</div>
</section>
<BlogAround prev={prev} next={next} />
</article>
{/* 评论互动 */}
<div className="duration-200 shadow w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-gray-800">
<Comment frontMatter={post} />
</div>
</div>)
}
const mapPageUrl = id => {
return 'https://www.notion.so/' + id.replace(/-/g, '')
}
function getMediumZoomMargin () {
const width = window.innerWidth
if (width < 500) {
return 8
} else if (width < 800) {
return 20
} else if (width < 1280) {
return 30
} else if (width < 1600) {
return 40
} else if (width < 1920) {
return 48
} else {
return 72
}
}

View File

@@ -0,0 +1,26 @@
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function BlogAround ({ prev, next }) {
if (!prev || !next) {
return <></>
}
return <section className='text-gray-800 border-t dark:text-gray-300 flex flex-wrap lg:flex-nowrap lg:space-x-10 justify-between py-2'>
<Link href={`/article/${prev.slug}`} passHref>
<a className='text-sm py-3 text-gray-400 hover:underline cursor-pointer'>
<FontAwesomeIcon icon={faAngleDoubleLeft} className='mr-1' />{prev.title}
</a>
</Link>
<Link href={`/article/${next.slug}`} passHref>
<a className='text-sm flex py-3 text-gray-400 hover:underline cursor-pointer'>{next.title}
<FontAwesomeIcon icon={faAngleDoubleRight} className='ml-1 my-1' />
</a>
</Link>
</section>
}

Some files were not shown because too many files have changed in this diff Show More