mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 15:09:22 +00:00
Merge branch 'main' into bug-fix-code-copy-repeat
This commit is contained in:
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,33 +1,33 @@
|
||||
---
|
||||
name: Bug report (Bug反馈)
|
||||
about: Create a report to help Nobelium get better.
|
||||
about: 报告一个软件的BUG来让NotionNext变得更好
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: tangly1024
|
||||
---
|
||||
|
||||
<!--
|
||||
!!! IMPORTANT !!!
|
||||
Please do not ignore this template. If you do, your issue will be closed.
|
||||
!!! 重要 !!!
|
||||
请遵守这个模板的格式填写,否则你的Issue将被关闭
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
**描述bug**
|
||||
简单说明bug的现象、相关的错误提示、日志等
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
**复现步骤**
|
||||
出现这个bug的操作步骤
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
**期望的正常结果**
|
||||
希望按这个步骤,正常操作结果是什么
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
**截图**
|
||||
相关的页面,应该用结果
|
||||
|
||||
**Platform:**
|
||||
**环境**
|
||||
|
||||
- OS: [e.g. iOS, Android, macOS]
|
||||
- Browser [e.g. chrome, safari, firefox]
|
||||
- Version [e.g. 22]
|
||||
- 操作系统: [例如. iOS, Android, macOS, windows]
|
||||
- 浏览器 [例如. chrome, safari, firefox]
|
||||
- 版本 [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
**补充说明**
|
||||
与问题相关的其它说明
|
||||
23
.github/ISSUE_TEMPLATE/deployment-error.md
vendored
23
.github/ISSUE_TEMPLATE/deployment-error.md
vendored
@@ -1,7 +1,28 @@
|
||||
---
|
||||
name: Deployment error (部署错误)
|
||||
about: Do you need help to install NotionNext?
|
||||
about: 在安装部署NotionNext时需要什么帮助吗
|
||||
title: ''
|
||||
labels: deployment
|
||||
assignees: tangly1024
|
||||
---
|
||||
|
||||
|
||||
<!--
|
||||
!!! 重要 !!!
|
||||
请遵守这个模板的格式填写,否则你的Issue将被关闭
|
||||
-->
|
||||
|
||||
**描述遇到的问题**
|
||||
简单说明你遇到的问题,相关的日志、错误信息
|
||||
|
||||
**相应配置**
|
||||
相关的配置,例如notion_page_id;你的网站地址
|
||||
|
||||
**截图**
|
||||
相关的页面,应该用结果
|
||||
|
||||
**环境**
|
||||
|
||||
- 操作系统: [例如. iOS, Android, macOS, windows]
|
||||
- 浏览器 [例如. chrome, safari, firefox]
|
||||
- 版本 [e.g. 22]
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,18 +7,18 @@ assignees: tangly1024
|
||||
---
|
||||
|
||||
<!--
|
||||
!!! IMPORTANT !!!
|
||||
Please do not ignore this template. If you do, your issue will be closed.
|
||||
!!! 重要 !!!
|
||||
请遵守这个模板的格式填写,否则你的Issue将被关闭
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
**为什么提出这个新的特性改动**
|
||||
简要说明此特性解决的问题,例如,『博客站点的读者互动性不够强,和读者无法建立紧密的联系...』
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
**描述一下你推荐的解决方案**
|
||||
简要说明你的解决方案建议,例如,『Giscus评论插件功能更加强大,用户只需留言既可在你的邮箱收到通知。。。』
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
**描述一下你考虑过的其它替代解决方案**
|
||||
简要说明你所有想过的有可能解决此问题的方案。
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
**补充说明**
|
||||
补充与此特性相关的内容
|
||||
|
||||
@@ -18,7 +18,7 @@ const BLOG = {
|
||||
FACEBOOK_PAGE_ID: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_ID || '', // Facebook Page ID 來啟用 messenger 聊天功能
|
||||
FACEBOOK_APP_ID: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '', // Facebook App ID 來啟用 messenger 聊天功能 获取: https://developers.facebook.com/
|
||||
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'next', // 主题, 支持 ['next','hexo',"fukasawa','medium']
|
||||
THEME: process.env.NEXT_PUBLIC_THEME || 'next', // 主题, 支持 ['next','hexo',"fukasawa','medium','example'] @see https://preview.tangly1024.com
|
||||
THEME_SWITCH: process.env.NEXT_PUBLIC_THEME_SWITCH || false, // 是否显示切换主题按钮
|
||||
LANG: 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
|
||||
HOME_BANNER_IMAGE: './bg_image.jpg', // 首页背景大图,默认文件:/public/bg_image.jpg 。会被Notion中的封面图覆盖。
|
||||
@@ -49,6 +49,9 @@ const BLOG = {
|
||||
CONTACT_GITHUB: 'https://github.com/tangly1024',
|
||||
CONTACT_TELEGRAM: '',
|
||||
|
||||
// 鼠标点击烟花特效
|
||||
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || true, // 鼠标点击烟花特效
|
||||
|
||||
// 悬浮挂件
|
||||
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
|
||||
WIDGET_PET_LINK:
|
||||
@@ -124,7 +127,7 @@ const BLOG = {
|
||||
process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述,被notion中的页面描述覆盖
|
||||
|
||||
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
|
||||
VERSION: '3.2.1' // 版本号
|
||||
VERSION: '3.3.1' // 版本号
|
||||
}
|
||||
|
||||
module.exports = BLOG
|
||||
|
||||
209
components/Fireworks.js
Normal file
209
components/Fireworks.js
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* https://codepen.io/juliangarnier/pen/gmOwJX
|
||||
* custom by hexo-theme-yun @YunYouJun
|
||||
*/
|
||||
import React from 'react'
|
||||
import anime from 'animejs'
|
||||
|
||||
export const Fireworks = () => {
|
||||
React.useEffect(() => {
|
||||
createFireworks({})
|
||||
}, [])
|
||||
return <canvas id='fireworks' className='fireworks'></canvas>
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建烟花
|
||||
* @param config
|
||||
*/
|
||||
function createFireworks(config) {
|
||||
const defaultColors = ['102, 167, 221', '62, 131, 225', '33, 78, 194']
|
||||
const defaultConfig = {
|
||||
colors: defaultColors,
|
||||
numberOfParticules: 20,
|
||||
orbitRadius: {
|
||||
min: 50,
|
||||
max: 100
|
||||
},
|
||||
circleRadius: {
|
||||
min: 10,
|
||||
max: 20
|
||||
},
|
||||
diffuseRadius: {
|
||||
min: 50,
|
||||
max: 100
|
||||
},
|
||||
animeDuration: {
|
||||
min: 900,
|
||||
max: 1500
|
||||
}
|
||||
}
|
||||
config = Object.assign(defaultConfig, config)
|
||||
|
||||
let pointerX = 0
|
||||
let pointerY = 0
|
||||
|
||||
// sky blue
|
||||
const colors = config.colors || defaultColors
|
||||
|
||||
const canvasEl = document.querySelector('.fireworks')
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
|
||||
/**
|
||||
* 设置画布尺寸
|
||||
*/
|
||||
function setCanvasSize(canvasEl) {
|
||||
canvasEl.width = window.innerWidth
|
||||
canvasEl.height = window.innerHeight
|
||||
canvasEl.style.width = `${window.innerWidth}px`
|
||||
canvasEl.style.height = `${window.innerHeight}px`
|
||||
}
|
||||
|
||||
/**
|
||||
* update pointer
|
||||
* @param {TouchEvent} e
|
||||
*/
|
||||
function updateCoords(e) {
|
||||
pointerX =
|
||||
e.clientX ||
|
||||
(e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
|
||||
pointerY =
|
||||
e.clientY ||
|
||||
(e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
|
||||
}
|
||||
|
||||
function setParticuleDirection(p) {
|
||||
const angle = (anime.random(0, 360) * Math.PI) / 180
|
||||
const value = anime.random(
|
||||
config.diffuseRadius.min,
|
||||
config.diffuseRadius.max
|
||||
)
|
||||
const radius = [-1, 1][anime.random(0, 1)] * value
|
||||
return {
|
||||
x: p.x + radius * Math.cos(angle),
|
||||
y: p.y + radius * Math.sin(angle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置创建粒子
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns
|
||||
*/
|
||||
function createParticule(x, y) {
|
||||
const p = {
|
||||
x,
|
||||
y,
|
||||
color: `rgba(${
|
||||
colors[anime.random(0, colors.length - 1)]
|
||||
},${
|
||||
anime.random(0.2, 0.8)
|
||||
})`,
|
||||
radius: anime.random(config.circleRadius.min, config.circleRadius.max),
|
||||
endPos: null,
|
||||
draw() {}
|
||||
}
|
||||
p.endPos = setParticuleDirection(p)
|
||||
p.draw = function() {
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
|
||||
ctx.fillStyle = p.color
|
||||
ctx.fill()
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function createCircle(x, y) {
|
||||
const p = {
|
||||
x,
|
||||
y,
|
||||
color: '#000',
|
||||
radius: 0.1,
|
||||
alpha: 0.5,
|
||||
lineWidth: 6,
|
||||
draw() {}
|
||||
}
|
||||
p.draw = function() {
|
||||
ctx.globalAlpha = p.alpha
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
|
||||
ctx.lineWidth = p.lineWidth
|
||||
ctx.strokeStyle = p.color
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function renderParticule(anim) {
|
||||
for (let i = 0; i < anim.animatables.length; i++) { anim.animatables[i].target.draw() }
|
||||
}
|
||||
|
||||
function animateParticules(x, y) {
|
||||
const circle = createCircle(x, y)
|
||||
const particules = []
|
||||
for (let i = 0; i < config.numberOfParticules; i++) { particules.push(createParticule(x, y)) }
|
||||
|
||||
anime
|
||||
.timeline()
|
||||
.add({
|
||||
targets: particules,
|
||||
x(p) {
|
||||
return p.endPos.x
|
||||
},
|
||||
y(p) {
|
||||
return p.endPos.y
|
||||
},
|
||||
radius: 0.1,
|
||||
duration: anime.random(
|
||||
config.animeDuration.min,
|
||||
config.animeDuration.max
|
||||
),
|
||||
easing: 'easeOutExpo',
|
||||
update: renderParticule
|
||||
})
|
||||
.add(
|
||||
{
|
||||
targets: circle,
|
||||
radius: anime.random(config.orbitRadius.min, config.orbitRadius.max),
|
||||
lineWidth: 0,
|
||||
alpha: {
|
||||
value: 0,
|
||||
easing: 'linear',
|
||||
duration: anime.random(600, 800)
|
||||
},
|
||||
duration: anime.random(1200, 1800),
|
||||
easing: 'easeOutExpo',
|
||||
update: renderParticule
|
||||
},
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
const render = anime({
|
||||
duration: Infinity,
|
||||
update: () => {
|
||||
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener(
|
||||
'mousedown',
|
||||
(e) => {
|
||||
render.play()
|
||||
updateCoords(e)
|
||||
animateParticules(pointerX, pointerY)
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
setCanvasSize(canvasEl)
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
() => {
|
||||
setCanvasSize(canvasEl)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
@@ -23,15 +23,20 @@ export default function Live2D() {
|
||||
return <canvas id="live2d" className='cursor-pointer' width="280" height="250" onClick={handleClick} alt='切换主题' title='切换主题' />
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载宠物
|
||||
*/
|
||||
function initLive2D() {
|
||||
// 加载 waifu.css live2d.min.js waifu-tips.js
|
||||
if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
// loadExternalResource('https://cdn.zhangxinxu.com/sp/demo/live2d/live2d/js/live2d.js', 'js')
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
// 加载 waifu.css live2d.min.js waifu-tips.js
|
||||
if (screen.width >= 768) {
|
||||
Promise.all([
|
||||
// loadExternalResource('https://cdn.zhangxinxu.com/sp/demo/live2d/live2d/js/live2d.js', 'js')
|
||||
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
|
||||
]).then((e) => {
|
||||
// https://github.com/xiazeyu/live2d-widget-models
|
||||
loadlive2d('live2d', BLOG.WIDGET_PET_LINK)
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NotionRenderer } from 'react-notion-x'
|
||||
import dynamic from 'next/dynamic'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
import React from 'react'
|
||||
|
||||
const Code = dynamic(() =>
|
||||
@@ -26,11 +27,32 @@ const NotionPage = ({ post }) => {
|
||||
return <>{post?.summary || ''}</>
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
addWatch4Dom()
|
||||
const zoom = typeof window !== 'undefined' && mediumZoom({
|
||||
container: '.notion-viewport',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
margin: getMediumZoomMargin()
|
||||
})
|
||||
|
||||
return <div id='container'>
|
||||
const zoomRef = React.useRef(zoom ? zoom.clone() : null)
|
||||
|
||||
React.useEffect(() => {
|
||||
addWatch4Dom()
|
||||
// 将相册gallery下的图片加入放大功能
|
||||
// const container = document?.getElementById('container')
|
||||
const imgList = document?.querySelectorAll('.notion-collection-card-cover img')
|
||||
if (imgList && zoomRef.current) {
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
(zoomRef.current).attach(imgList[i])
|
||||
}
|
||||
}
|
||||
|
||||
const cards = document.getElementsByClassName('notion-collection-card')
|
||||
for (const e of cards) {
|
||||
e.removeAttribute('href')
|
||||
}
|
||||
})
|
||||
|
||||
return <div id='container' className='max-w-4xl mx-auto'>
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
@@ -44,8 +66,6 @@ const NotionPage = ({ post }) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
export default NotionPage
|
||||
|
||||
/**
|
||||
* 监听DOM变化
|
||||
* @param {*} element
|
||||
@@ -116,3 +136,23 @@ const mapPageUrl = id => {
|
||||
// return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
return '/article/' + 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
|
||||
}
|
||||
}
|
||||
|
||||
export default NotionPage
|
||||
|
||||
@@ -49,7 +49,7 @@ export function GlobalContextProvider({ children }) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ onLoading, locale, updateLocale, isDarkMode, updateDarkMode, theme, setTheme, switchTheme, changeTheme }}>
|
||||
<GlobalContext.Provider value={{ onLoading, changeLoadingState, locale, updateLocale, isDarkMode, updateDarkMode, theme, setTheme, switchTheme, changeTheme }}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { getAllPosts } from './notion/getAllPosts'
|
||||
export { getAllTags } from './notion/getAllTags'
|
||||
export { getPostBlocks } from './notion/getPostBlocks'
|
||||
export { getAllCategories } from './notion/getAllCategories'
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { isIterable } from '../utils'
|
||||
|
||||
/**
|
||||
* 获取所有文章的分类
|
||||
* @param allPosts
|
||||
* @returns {Promise<{}|*[]>}
|
||||
*/
|
||||
export async function getAllCategories({ allPosts, categoryOptions, sliceCount = 0 }) {
|
||||
if (!allPosts || !categoryOptions) {
|
||||
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
|
||||
}
|
||||
})
|
||||
const list = []
|
||||
if (isIterable(categoryOptions)) {
|
||||
for (const c of categoryOptions) {
|
||||
const count = categoryObj[c.value]
|
||||
if (count) {
|
||||
list.push({ id: c.id, name: c.value, color: c.color, count })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按照数量排序
|
||||
// list.sort((a, b) => b.count - a.count)
|
||||
if (sliceCount && sliceCount > 0) {
|
||||
return list.slice(0, sliceCount)
|
||||
} else {
|
||||
return list
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export async function getAllPosts({ notionPageData, from, pageType }) {
|
||||
|
||||
const pageBlock = notionPageData.block
|
||||
const schema = notionPageData.schema
|
||||
const tagOptions = notionPageData.tagOptions
|
||||
const tagOptions = notionPageData.tags
|
||||
const collectionQuery = notionPageData.collectionQuery
|
||||
|
||||
const data = []
|
||||
@@ -73,19 +73,3 @@ function getPostCover(id, block) {
|
||||
if (pageCover.startsWith('http')) return defaultMapImageUrl(pageCover, block[id].value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取博文总数
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export async function getAllPostCount({ notionPageData, from }) {
|
||||
if (!notionPageData) {
|
||||
notionPageData = await getNotionPageData({ from })
|
||||
}
|
||||
if (!notionPageData) {
|
||||
return []
|
||||
}
|
||||
const allPosts = await getAllPosts({ notionPageData, from, pageType: ['Post'] })
|
||||
return allPosts?.length || 0
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isIterable } from '../utils'
|
||||
* @param tagOptions tags的下拉选项
|
||||
* @returns {Promise<{}|*[]>}
|
||||
*/
|
||||
export async function getAllTags({ allPosts, sliceCount = 0, tagOptions }) {
|
||||
export function getAllTags({ allPosts, sliceCount = 0, tagOptions }) {
|
||||
if (!allPosts || !tagOptions) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
|
||||
import { getPostBlocks } from '@/lib/notion/getPostBlocks'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import { defaultMapImageUrl } from 'react-notion-x'
|
||||
import { getAllCategories } from './getAllCategories'
|
||||
import { getAllPosts, getAllPostCount } from './getAllPosts'
|
||||
import { deepClone, isIterable } from '../utils'
|
||||
import getAllPageIds from './getAllPageIds'
|
||||
import { getAllPosts } from './getAllPosts'
|
||||
import { getAllTags } from './getAllTags'
|
||||
import getPageProperties from './getPageProperties'
|
||||
|
||||
/**
|
||||
* 获取博客数据
|
||||
@@ -15,40 +17,29 @@ import { getAllTags } from './getAllTags'
|
||||
* @param categoryCount
|
||||
* @param tagsCount 截取标签数量
|
||||
* @param pageType 过滤的文章类型,数组格式 ['Page','Post']
|
||||
* @returns {
|
||||
allPosts, 所有博客
|
||||
latestPosts,
|
||||
categories, 所有分类
|
||||
postCount,
|
||||
customNav, 自定义导航菜单
|
||||
tags, 所有标签
|
||||
siteInfo:{
|
||||
title,
|
||||
description,
|
||||
pageCover
|
||||
}
|
||||
}
|
||||
* @returns
|
||||
*
|
||||
*/
|
||||
export async function getGlobalNotionData({
|
||||
pageId = BLOG.NOTION_PAGE_ID,
|
||||
from,
|
||||
latestPostCount = 5,
|
||||
categoryCount = BLOG.PREVIEW_CATEGORY_COUNT,
|
||||
tagsCount = BLOG.PREVIEW_TAG_COUNT,
|
||||
pageType = ['Post']
|
||||
}) {
|
||||
const notionPageData = await getNotionPageData({ pageId, from })
|
||||
const siteInfo = await getBlogInfo({ notionPageData, from })
|
||||
const tagOptions = notionPageData.tagOptions
|
||||
const categoryOptions = notionPageData.categoryOptions
|
||||
const customNav = await getCustomNav({ notionPageData })
|
||||
// 深拷贝数据
|
||||
const notionPageData = deepClone(await getNotionPageData({ pageId, from }))
|
||||
|
||||
const allPosts = await getAllPosts({ notionPageData, from, pageType })
|
||||
const postCount = await getAllPostCount({ notionPageData, from })
|
||||
const categories = await getAllCategories({ allPosts, categoryOptions, sliceCount: categoryCount })
|
||||
const tags = await getAllTags({ allPosts, tagOptions, sliceCount: tagsCount })
|
||||
const latestPosts = await getLatestPosts({ notionPageData, from, latestPostCount })
|
||||
return { allPosts, latestPosts, categories, postCount, customNav, tags, siteInfo }
|
||||
notionPageData.allPosts = allPosts
|
||||
// 删除前端不需要的数据
|
||||
delete notionPageData.block
|
||||
delete notionPageData.collection
|
||||
delete notionPageData.collectionQuery
|
||||
delete notionPageData.schema
|
||||
delete notionPageData.rawMetadata
|
||||
delete notionPageData.pageIds
|
||||
delete notionPageData.tagOptions
|
||||
delete notionPageData.categoryOptions
|
||||
return notionPageData
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,8 +47,7 @@ export async function getGlobalNotionData({
|
||||
* @param {*}} param0
|
||||
* @returns
|
||||
*/
|
||||
async function getLatestPosts({ notionPageData, from, latestPostCount }) {
|
||||
const allPosts = await getAllPosts({ notionPageData, from, pageType: ['Post'] })
|
||||
function getLatestPosts({ allPosts, from, latestPostCount }) {
|
||||
const latestPosts = Object.create(allPosts).sort((a, b) => {
|
||||
const dateA = new Date(a?.lastEditedTime || a?.createdTime || a?.date?.start_date)
|
||||
const dateB = new Date(b?.lastEditedTime || b?.createdTime || b?.date?.start_date)
|
||||
@@ -79,7 +69,7 @@ export async function getNotionPageData({ pageId, from }) {
|
||||
// 尝试从缓存获取
|
||||
const cacheKey = 'page_block_' + pageId
|
||||
const data = await getDataFromCache(cacheKey)
|
||||
if (data) {
|
||||
if (data && data.pageIds?.length > 0) {
|
||||
console.log('[请求缓存]:', `from:${from}`, `root-page-id:${pageId}`)
|
||||
return data
|
||||
}
|
||||
@@ -96,14 +86,7 @@ export async function getNotionPageData({ pageId, from }) {
|
||||
* @param notionPageData
|
||||
* @returns {Promise<[]|*[]>}
|
||||
*/
|
||||
async function getCustomNav({ notionPageData }) {
|
||||
if (!notionPageData) {
|
||||
notionPageData = await getNotionPageData({ from: 'custom-nav' })
|
||||
}
|
||||
if (!notionPageData) {
|
||||
return []
|
||||
}
|
||||
const allPage = await getAllPosts({ notionPageData, from: 'custom-nav', pageType: ['Page'] })
|
||||
function getCustomNav({ allPage }) {
|
||||
const customNav = []
|
||||
if (allPage && allPage.length > 0) {
|
||||
allPage.forEach(p => {
|
||||
@@ -139,23 +122,55 @@ function getCategoryOptions(schema) {
|
||||
return categorySchema?.options || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有文章的分类
|
||||
* @param allPosts
|
||||
* @returns {Promise<{}|*[]>}
|
||||
*/
|
||||
function getAllCategories({ allPosts, categoryOptions, sliceCount = 0 }) {
|
||||
if (!allPosts || !categoryOptions) {
|
||||
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
|
||||
}
|
||||
})
|
||||
const list = []
|
||||
if (isIterable(categoryOptions)) {
|
||||
for (const c of categoryOptions) {
|
||||
const count = categoryObj[c.value]
|
||||
if (count) {
|
||||
list.push({ id: c.id, name: c.value, color: c.color, count })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按照数量排序
|
||||
// list.sort((a, b) => b.count - a.count)
|
||||
if (sliceCount && sliceCount > 0) {
|
||||
return list.slice(0, sliceCount)
|
||||
} else {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 站点信息
|
||||
* @param notionPageData
|
||||
* @param from
|
||||
* @returns {Promise<{title,description,pageCover}>}
|
||||
*/
|
||||
async function getBlogInfo({ notionPageData, from }) {
|
||||
if (!notionPageData) {
|
||||
notionPageData = await getNotionPageData({ from })
|
||||
}
|
||||
if (!notionPageData) {
|
||||
return null
|
||||
}
|
||||
const collection = notionPageData?.collection
|
||||
function getBlogInfo({ collection, block }) {
|
||||
const title = collection?.name?.[0][0] || BLOG.TITLE
|
||||
const description = collection?.description?.[0][0] || BLOG.DESCRIPTION
|
||||
const pageCover = mapCoverUrl(collection?.cover, notionPageData.block)
|
||||
const pageCover = mapCoverUrl(collection?.cover, block)
|
||||
return { title, description, pageCover }
|
||||
}
|
||||
|
||||
@@ -184,23 +199,47 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
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)
|
||||
const categoryOptions = getCategoryOptions(schema)
|
||||
|
||||
const rawMetadata = block[pageId]?.value
|
||||
// Check Type Page-Database和Inline-Database
|
||||
if (
|
||||
rawMetadata?.type !== 'collection_view_page' &&
|
||||
rawMetadata?.type !== 'collection_view'
|
||||
rawMetadata?.type !== 'collection_view'
|
||||
) {
|
||||
console.warn(`pageId "${pageId}" is not a database`)
|
||||
return null
|
||||
}
|
||||
|
||||
const collection = Object.values(pageRecordMap.collection)[0]?.value
|
||||
const collectionQuery = pageRecordMap.collection_query
|
||||
const schema = collection?.schema
|
||||
const tagOptions = getTagOptions(schema)
|
||||
const categoryOptions = getCategoryOptions(schema)
|
||||
|
||||
const data = []
|
||||
const pageIds = getAllPageIds(collectionQuery)
|
||||
for (let i = 0; i < pageIds.length; i++) {
|
||||
const id = pageIds[i]
|
||||
const properties = (await getPageProperties(id, block, schema)) || null
|
||||
properties.slug = properties.slug ?? properties.id
|
||||
delete properties.content
|
||||
data.push(properties)
|
||||
}
|
||||
|
||||
const allPage = data.filter(post => {
|
||||
return post.title && post?.status?.[0] === 'Published' && ['Page'].indexOf(post?.type?.[0]) > -1
|
||||
})
|
||||
const allPosts = data.filter(post => {
|
||||
return post.title && post?.status?.[0] === 'Published' && ['Post'].indexOf(post?.type?.[0]) > -1
|
||||
})
|
||||
|
||||
const customNav = getCustomNav({ allPage })
|
||||
const postCount = allPosts?.length || 0
|
||||
const categories = getAllCategories({ allPosts, categoryOptions, sliceCount: BLOG.PREVIEW_CATEGORY_COUNT })
|
||||
const tags = getAllTags({ allPosts, tagOptions, sliceCount: BLOG.PREVIEW_TAG_COUNT })
|
||||
const latestPosts = getLatestPosts({ allPosts, from, latestPostCount: 5 })
|
||||
const siteInfo = getBlogInfo({ collection, block })
|
||||
|
||||
return {
|
||||
collection,
|
||||
collectionQuery,
|
||||
@@ -208,6 +247,13 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
schema,
|
||||
tagOptions,
|
||||
categoryOptions,
|
||||
rawMetadata
|
||||
rawMetadata,
|
||||
siteInfo,
|
||||
customNav,
|
||||
postCount,
|
||||
pageIds,
|
||||
categories,
|
||||
tags,
|
||||
latestPosts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { NotionAPI } from 'notion-client'
|
||||
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
|
||||
import { deepClone, delay } from '../utils'
|
||||
|
||||
export async function getPostBlocks(id, from, slice, retryCount = 3) {
|
||||
export async function getPostBlocks(id, from, slice) {
|
||||
const cacheKey = 'page_block_' + id
|
||||
let pageBlock = await getDataFromCache(cacheKey)
|
||||
if (pageBlock) {
|
||||
console.log('[请求缓存]:', `from:${from}`, `id:${id}`, cacheKey)
|
||||
console.log('[请求缓存]:', `from:${from}`, cacheKey)
|
||||
return filterPostBlocks(id, pageBlock, slice)
|
||||
}
|
||||
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
|
||||
const api = new NotionAPI({ authToken })
|
||||
try {
|
||||
console.warn('[请求API]:', `from:${from}`, `id:${id}`)
|
||||
pageBlock = await api.getPage(id)
|
||||
console.warn('[请求成功]', `from:${from}`, `id:${id}`)
|
||||
} catch (error) {
|
||||
console.error('[请求失败]', `from:${from}`, `id:${id}`, `error:${error}`)
|
||||
if (retryCount && retryCount > 0) { // 重试
|
||||
console.error('[重试请求]', `from:${from}`, `id:${id}`, `剩余次数:${retryCount}`)
|
||||
return getPostBlocks(id, from, slice, retryCount - 1)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
console.warn('[请求API]:', `from:${from}`, `id:${id}`)
|
||||
pageBlock = await getPageWithRetry(id, from)
|
||||
console.warn('[请求完成]: 结果', `${pageBlock ? '成功' : '失败'}`, `from:${from}`, `id:${id}`)
|
||||
|
||||
if (pageBlock) {
|
||||
await setDataToCache(cacheKey, pageBlock)
|
||||
@@ -31,6 +22,33 @@ export async function getPostBlocks(id, from, slice, retryCount = 3) {
|
||||
return pageBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用接口,失败会重试
|
||||
* @param {*} id
|
||||
* @param {*} retryAttempts
|
||||
*/
|
||||
async function getPageWithRetry(id, from, retryAttempts = 3) {
|
||||
if (retryAttempts && retryAttempts > 0) {
|
||||
console.error('[发起请求]', `from:${from}`, `id:${id}`, `剩余重试次数:${retryAttempts}`)
|
||||
try {
|
||||
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
|
||||
const api = new NotionAPI({ authToken })
|
||||
return await api.getPage(id)
|
||||
} catch (e) {
|
||||
await delay(1000)
|
||||
const cacheKey = 'page_block_' + id
|
||||
const pageBlock = await getDataFromCache(cacheKey)
|
||||
if (pageBlock) {
|
||||
console.error('[重试获取缓存]', `from:${from}`, `id:${id}`)
|
||||
return pageBlock
|
||||
}
|
||||
return await getPageWithRetry(id, from, retryAttempts - 1)
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到的blockMap删除不需要的字段
|
||||
* @param {*} id 页面ID
|
||||
@@ -49,6 +67,16 @@ function filterPostBlocks(id, pageBlock, slice) {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
// 处理 c++ 和 c#两种语言
|
||||
if (b?.value?.type === 'code') {
|
||||
if (b?.value?.properties?.language?.[0][0] === 'C++') {
|
||||
b.value.properties.language[0][0] = 'cpp'
|
||||
}
|
||||
if (b?.value?.properties?.language?.[0][0] === 'C#') {
|
||||
b.value.properties.language[0][0] = 'csharp'
|
||||
}
|
||||
}
|
||||
|
||||
delete b?.role
|
||||
delete b?.value?.version
|
||||
delete b?.value?.created_by_table
|
||||
@@ -64,15 +92,3 @@ function filterPostBlocks(id, pageBlock, slice) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
19
lib/utils.js
19
lib/utils.js
@@ -80,3 +80,22 @@ export function isObject(item) {
|
||||
export function isIterable(obj) {
|
||||
return obj != null && typeof obj[Symbol.iterator] === 'function'
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 延时
|
||||
* @param {*} ms
|
||||
* @returns
|
||||
*/
|
||||
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "notion-next",
|
||||
"version": "3.2.1",
|
||||
"version": "3.3.1",
|
||||
"homepage": "https://github.com/tangly1024/NotionNext.git",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -24,6 +24,7 @@
|
||||
"@next/bundle-analyzer": "^12.1.1",
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"animate.css": "^4.1.1",
|
||||
"animejs": "^3.2.1",
|
||||
"axios": ">=0.21.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"feed": "^4.2.2",
|
||||
|
||||
@@ -3,7 +3,8 @@ import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面,针对类型为Page的页面
|
||||
@@ -11,16 +12,32 @@ import { useEffect, useState } from 'react'
|
||||
* @returns
|
||||
*/
|
||||
const Slug = props => {
|
||||
const { theme } = useGlobal()
|
||||
const { theme, changeLoadingState } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { post } = props
|
||||
|
||||
if (!post) {
|
||||
return <ThemeComponents.Layout404 {...props} />
|
||||
changeLoadingState(true)
|
||||
const router = useRouter()
|
||||
setTimeout(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
const meta = { title: `${props?.siteInfo?.title || BLOG.TITLE} | loading` }
|
||||
return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
|
||||
}
|
||||
|
||||
changeLoadingState(false)
|
||||
|
||||
// 文章锁🔐
|
||||
const [lock, setLock] = useState(post.password && post.password !== '')
|
||||
useEffect(() => {
|
||||
const [lock, setLock] = React.useState(post.password && post.password !== '')
|
||||
React.useEffect(() => {
|
||||
if (post.password && post.password !== '') {
|
||||
setLock(true)
|
||||
} else {
|
||||
@@ -40,12 +57,13 @@ const Slug = props => {
|
||||
|
||||
const { siteInfo } = props
|
||||
const meta = {
|
||||
title: `${post.title} | ${siteInfo.title}`,
|
||||
description: post.summary,
|
||||
title: `${post?.title} | ${siteInfo?.title}`,
|
||||
description: post?.summary,
|
||||
type: 'article',
|
||||
image: post.page_cover,
|
||||
slug: post.slug,
|
||||
tags: post.tags
|
||||
slug: 'article/' + post?.slug,
|
||||
image: post?.page_cover,
|
||||
category: post?.category?.[0],
|
||||
tags: post?.tags
|
||||
}
|
||||
|
||||
props = { ...props, meta, lock, setLock, validPassword }
|
||||
|
||||
@@ -8,15 +8,14 @@ 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 'prismjs/themes/prism-tomorrow.min.css'
|
||||
import 'react-notion-x/build/third-party/equation.css'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { GlobalContextProvider } from '@/lib/global'
|
||||
import { DebugPanel } from '@/components/DebugPanel'
|
||||
import { ThemeSwitch } from '@/components/ThemeSwitch'
|
||||
import { Fireworks } from '@/components/Fireworks'
|
||||
|
||||
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
|
||||
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
|
||||
@@ -29,23 +28,25 @@ const Messenger = dynamic(() => import('@/components/FacebookMessenger'), {
|
||||
})
|
||||
|
||||
const MyApp = ({ Component, pageProps }) => {
|
||||
// 外部插件
|
||||
const externalPlugins = <>
|
||||
{JSON.parse(BLOG.THEME_SWITCH) && <ThemeSwitch />}
|
||||
{JSON.parse(BLOG.DEBUG) && <DebugPanel />}
|
||||
{BLOG.ANALYTICS_ACKEE_TRACKER && <Ackee />}
|
||||
{BLOG.ANALYTICS_GOOGLE_ID && <Gtag />}
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <Busuanzi />}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <GoogleAdsense />}
|
||||
{BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && <Messenger />}
|
||||
{JSON.parse(BLOG.FIREWORKS) && <Fireworks/>}
|
||||
</>
|
||||
|
||||
return (
|
||||
<GlobalContextProvider>
|
||||
{BLOG.THEME_SWITCH && <ThemeSwitch />}
|
||||
{BLOG.DEBUG && <DebugPanel />}
|
||||
{BLOG.ANALYTICS_ACKEE_TRACKER && <Ackee />}
|
||||
{BLOG.ANALYTICS_GOOGLE_ID && <Gtag />}
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <Busuanzi />}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <GoogleAdsense />}
|
||||
{BLOG.FACEBOOK_APP_ID && BLOG.FACEBOOK_PAGE_ID && <Messenger />}
|
||||
{/* FontawesomeCDN */}
|
||||
<link
|
||||
href={BLOG.FONT_AWESOME_PATH}
|
||||
rel="stylesheet"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
</GlobalContextProvider>
|
||||
<GlobalContextProvider>
|
||||
{/* FontawesomeCDN */}
|
||||
<link rel="stylesheet" href={BLOG.FONT_AWESOME_PATH} referrerPolicy="no-referrer" />
|
||||
{externalPlugins}
|
||||
<Component {...pageProps} />
|
||||
</GlobalContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ const ArchiveIndex = props => {
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { siteInfo } = props
|
||||
const meta = {
|
||||
title: `${locale.NAV.ARCHIVE} | ${siteInfo.title}`,
|
||||
description: siteInfo.description,
|
||||
title: `${locale.NAV.ARCHIVE} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
slug: 'archive',
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { getPostBlocks } from '@/lib/notion'
|
||||
import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 根据notion的slug访问页面
|
||||
@@ -13,32 +13,33 @@ import { idToUuid } from 'notion-utils'
|
||||
* @returns
|
||||
*/
|
||||
const Slug = props => {
|
||||
const { theme } = useGlobal()
|
||||
const { theme, changeLoadingState } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
const { post } = props
|
||||
|
||||
if (!post) {
|
||||
changeLoadingState(true)
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const article = document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
}, 10000)
|
||||
const meta = { title: `${props?.siteInfo?.title || BLOG.TITLE} | loading` }
|
||||
return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
|
||||
}
|
||||
|
||||
changeLoadingState(false)
|
||||
|
||||
// 文章锁🔐
|
||||
const [lock, setLock] = useState(post.password && post.password !== '')
|
||||
useEffect(() => {
|
||||
if (post.password && post.password !== '') {
|
||||
const [lock, setLock] = React.useState(post?.password && post?.password !== '')
|
||||
React.useEffect(() => {
|
||||
if (post?.password && post?.password !== '') {
|
||||
setLock(true)
|
||||
} else {
|
||||
setLock(false)
|
||||
@@ -46,9 +47,9 @@ const Slug = props => {
|
||||
}, [post])
|
||||
|
||||
/**
|
||||
* 验证文章密码
|
||||
* @param {*} result
|
||||
*/
|
||||
* 验证文章密码
|
||||
* @param {*} result
|
||||
*/
|
||||
const validPassword = result => {
|
||||
if (result) {
|
||||
setLock(false)
|
||||
@@ -59,13 +60,13 @@ const Slug = props => {
|
||||
|
||||
const { siteInfo } = props
|
||||
const meta = {
|
||||
title: `${props.post.title} | ${siteInfo.title}`,
|
||||
description: props.post.summary,
|
||||
title: `${post?.title} | ${siteInfo?.title}`,
|
||||
description: post?.summary,
|
||||
type: 'article',
|
||||
slug: 'article/' + props.post.slug,
|
||||
image: props.post.page_cover,
|
||||
category: props.post.category?.[0],
|
||||
tags: props.post.tags
|
||||
slug: 'article/' + post?.slug,
|
||||
image: post?.page_cover,
|
||||
category: post?.category?.[0],
|
||||
tags: post?.tags
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,8 +9,8 @@ export default function Category(props) {
|
||||
const { locale } = useGlobal()
|
||||
const { siteInfo } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.CATEGORY} | ${siteInfo.title}`,
|
||||
description: siteInfo.description,
|
||||
title: `${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
slug: 'category',
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
export async function getServerSideProps ({ res }) {
|
||||
res.setHeader('Content-Type', 'text/xml')
|
||||
// 获取最新文章
|
||||
const globalNotionData = await getGlobalNotionData({ from: 'rss', latestPostCount: 5 })
|
||||
const globalNotionData = await getGlobalNotionData({ from: 'rss' })
|
||||
const xmlFeed = await generateRss(globalNotionData?.latestPosts || [])
|
||||
res.write(xmlFeed)
|
||||
res.end()
|
||||
|
||||
@@ -14,9 +14,9 @@ export async function getStaticProps() {
|
||||
const props = await getGlobalNotionData({ from, pageType: ['Post'] })
|
||||
const { allPosts, siteInfo } = props
|
||||
const meta = {
|
||||
title: `${siteInfo.title} | ${siteInfo.description}`,
|
||||
description: siteInfo.description,
|
||||
image: siteInfo.pageCover,
|
||||
title: `${siteInfo?.title} | ${siteInfo?.description}`,
|
||||
description: siteInfo?.description,
|
||||
image: siteInfo?.pageCover,
|
||||
slug: '',
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ const Page = props => {
|
||||
return <></>
|
||||
}
|
||||
const meta = {
|
||||
title: `${props.page} | Page | ${siteInfo.title}`,
|
||||
description: siteInfo.description,
|
||||
title: `${props.page} | Page | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
slug: 'page/' + props.page,
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ const Search = props => {
|
||||
const { locale } = useGlobal()
|
||||
const meta = {
|
||||
title: `${searchKey || ''}${searchKey ? ' | ' : ''}${locale.NAV.SEARCH} | ${
|
||||
siteInfo.title
|
||||
siteInfo?.title
|
||||
}`,
|
||||
description: siteInfo.description,
|
||||
description: siteInfo?.description,
|
||||
slug: 'search',
|
||||
type: 'website'
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ function getTagNames(tags) {
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const from = 'tag-static-path'
|
||||
const { tags } = await getGlobalNotionData({ from, tagsCount: 0 })
|
||||
const { tags } = await getGlobalNotionData({ from })
|
||||
const tagNames = getTagNames(tags)
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,8 +9,8 @@ const TagIndex = props => {
|
||||
const { locale } = useGlobal()
|
||||
const { siteInfo } = props
|
||||
const meta = {
|
||||
title: `${locale.COMMON.TAGS} | ${siteInfo.title}`,
|
||||
description: siteInfo.description,
|
||||
title: `${locale.COMMON.TAGS} | ${siteInfo?.title}`,
|
||||
description: siteInfo?.description,
|
||||
slug: 'tag',
|
||||
type: 'website'
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const TagIndex = props => {
|
||||
|
||||
export async function getStaticProps() {
|
||||
const from = 'tag-index-props'
|
||||
const props = await getGlobalNotionData({ from, tagsCount: 0 })
|
||||
const props = await getGlobalNotionData({ from })
|
||||
return {
|
||||
props,
|
||||
revalidate: 1
|
||||
|
||||
@@ -145,4 +145,12 @@ nav {
|
||||
|
||||
.notion-code-copy-button > svg{
|
||||
pointer-events:none
|
||||
}
|
||||
}
|
||||
|
||||
.fireworks{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -530,8 +530,7 @@
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
border-color: var(--fg-color-0);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.notion-link {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const Layout404 = () => {
|
||||
return <div>
|
||||
404 Not found.
|
||||
</div>
|
||||
export const Layout404 = (props) => {
|
||||
return <LayoutBase {...props}>
|
||||
404 Not found.
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -24,15 +24,13 @@ export const LayoutArchive = props => {
|
||||
})
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<div className="mb-10 pb-20 md:p-12 p-3 min-h-full">
|
||||
<div className="mb-10 pb-20 md:py-12 p-3 min-h-screen w-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<div key={archiveTitle}>
|
||||
<div
|
||||
className="pt-16 pb-4 text-3xl dark:text-gray-300"
|
||||
id={archiveTitle}
|
||||
>
|
||||
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
|
||||
{archiveTitle}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{archivePosts[archiveTitle].map(post => (
|
||||
<li
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { Header } from './components/Header'
|
||||
import { Nav } from './components/Nav'
|
||||
import { Footer } from './components/Footer'
|
||||
import { Title } from './components/Title'
|
||||
import { SideBar } from './components/SideBar'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
|
||||
@@ -11,116 +13,37 @@ import { useGlobal } from '@/lib/global'
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, meta, customNav, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
|
||||
|
||||
let links = [
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
const { children, meta } = props
|
||||
return (
|
||||
<div className='dark:text-gray-300'>
|
||||
<CommonHead meta={meta} />
|
||||
{/* 导航菜单 */}
|
||||
<div className="w-full flex justify-center my-2 text-xs md:text-base px-5">
|
||||
<div className="max-w-6xl justify-between w-full flex">
|
||||
<section>
|
||||
<Link title={siteInfo.title} href={'/'}>
|
||||
<a className={'cursor-pointer flex items-center hover:underline'}>
|
||||
<i className={'fas fa-home mr-1'} />
|
||||
<div className="text-center">{siteInfo.title} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
<nav className="space-x-3 flex flex-nowrap overflow-x-auto">
|
||||
{links.map(link => {
|
||||
if (link) {
|
||||
return (
|
||||
<Link key={`${link.to}`} title={link.to} href={link.to}>
|
||||
<a
|
||||
className={
|
||||
'cursor-pointer flex whitespace-nowrap items-center hover:underline'
|
||||
}
|
||||
>
|
||||
<i className={`${link.icon} mr-1`} />
|
||||
<div className="text-center">{link.name}</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div className='dark:text-gray-300'>
|
||||
<CommonHead meta={meta} />
|
||||
{/* 顶栏LOGO */}
|
||||
<Header {...props} />
|
||||
|
||||
{/* 内容主体 */}
|
||||
<main id="wrapper" className="flex justify-center flex-1 pb-12">
|
||||
<div className="max-w-4xl w-full px-3">{children}</div>
|
||||
<div className='hidden md:block'>
|
||||
<div className="sticky top-0 z-40">
|
||||
<Live2D />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/* 菜单 */}
|
||||
<Nav {...props} />
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer className="font-sans dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-sm p-6">
|
||||
<i className="fas fa-copyright" /> {`${startYear}${currentYear}`}{' '}
|
||||
<span>
|
||||
<i className="mx-1 animate-pulse fas fa-heart" />{' '}
|
||||
<a
|
||||
href={BLOG.LINK}
|
||||
className="underline dark:text-gray-300 "
|
||||
>
|
||||
{BLOG.AUTHOR}
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
{BLOG.BEI_AN && (
|
||||
<>
|
||||
<br />
|
||||
<i className="fas fa-shield-alt" />{' '}
|
||||
<a href="https://beian.miit.gov.cn/" className="mr-2">
|
||||
{BLOG.BEI_AN}
|
||||
</a>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<span className="hidden busuanzi_container_site_pv">
|
||||
<i className="fas fa-eye" />
|
||||
<span className="px-1 busuanzi_value_site_pv"> </span>{' '}
|
||||
</span>
|
||||
<span className="pl-2 hidden busuanzi_container_site_uv">
|
||||
<i className="fas fa-users" />{' '}
|
||||
<span className="px-1 busuanzi_value_site_uv"> </span>{' '}
|
||||
</span>
|
||||
<br />
|
||||
<h1>{meta?.title || siteInfo.title}</h1>
|
||||
<span className='text-xs font-serif'>
|
||||
Powered by{' '}
|
||||
<a
|
||||
href="https://github.com/tangly1024/NotionNext"
|
||||
className="underline dark:text-gray-300"
|
||||
>
|
||||
NotionNext {BLOG.VERSION}
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
{/* 主体 */}
|
||||
<div className="w-full bg-white">
|
||||
|
||||
<Title {...props} />
|
||||
|
||||
<div className="container mx-auto justify-center md:flex items-start py-8 px-2">
|
||||
|
||||
<div className='w-full max-w-3xl xl:px-14 lg:px-4 '>{children}</div>
|
||||
|
||||
<SideBar {...props} />
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Footer {...props} />
|
||||
|
||||
<div className='fixed right-4 bottom-4'>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,26 +23,39 @@ export const LayoutCategory = props => {
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
Category - {category}
|
||||
{postsToShow.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
return <LayoutBase {...props}>
|
||||
<div className='w-full'>
|
||||
<div className='pb-12'>{category}</div>
|
||||
|
||||
{postsToShow.map(p => (
|
||||
<article key={p.id} className="mb-12" >
|
||||
<h2 className="mb-4">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="text-black text-xl md:text-2xl no-underline hover:underline"> {p.title}</a>
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700">
|
||||
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href="#" className="text-gray-700">{p.category}</a>
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-normal">
|
||||
{p.summary}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
</LayoutBase >
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutCategoryIndex = (props) => {
|
||||
const { categories } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <LayoutBase {...props}>
|
||||
<div className=' p-10'>
|
||||
<div className='dark:text-gray-200 mb-5'>
|
||||
<i className='mr-4 fas fa-th' />{locale.COMMON.CATEGORY}:
|
||||
</div>
|
||||
<div >
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categories && categories.map(category => {
|
||||
return <Link key={category.name} href={`/category/${category.name}`} passHref>
|
||||
|
||||
@@ -1,74 +1,11 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { BlogList } from './components/BlogList'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutIndex = props => {
|
||||
const { posts, postCount } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
const page = 1
|
||||
const showNext =
|
||||
page < totalPage &&
|
||||
posts.length === BLOG.POSTS_PER_PAGE &&
|
||||
posts.length < postCount
|
||||
|
||||
const currentPage = +page
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
{posts.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="border dark:border-hexo-black-gray p-4 my-12"
|
||||
>
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${BLOG.SUB_PATH || '/'}`
|
||||
: `/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
rel="prev"
|
||||
className={`${
|
||||
currentPage === 1 ? 'invisible' : 'visible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
rel="next"
|
||||
className={`${
|
||||
showNext ? 'visible' : 'invisible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<BlogList {...props} page={1} />
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,71 +1,10 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { BlogList } from './components/BlogList'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutPage = props => {
|
||||
const { page } = props
|
||||
const { posts, postCount } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
const showNext =
|
||||
page < totalPage &&
|
||||
posts.length === BLOG.POSTS_PER_PAGE &&
|
||||
posts.length < postCount
|
||||
|
||||
const currentPage = +page
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
{posts.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${BLOG.SUB_PATH || '/'}`
|
||||
: `/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
rel="prev"
|
||||
className={`${
|
||||
currentPage === 1 ? 'invisible' : 'visible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
rel="next"
|
||||
className={`${
|
||||
showNext ? 'visible' : 'invisible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<BlogList {...props} />
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SearchInput from './components/SearchInput'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSearch = props => {
|
||||
const { keyword, posts } = props
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('container')
|
||||
const container = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${keyword}</span>`)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}, [router.events])
|
||||
|
||||
const { locale } = useGlobal()
|
||||
|
||||
@@ -33,28 +36,58 @@ export const LayoutSearch = props => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (keyword) {
|
||||
const targets = document.getElementsByClassName('replace')
|
||||
for (const container of targets) {
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(
|
||||
re,
|
||||
`<span class='text-red-500 border-b border-dashed'>${keyword}</span>`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<h2>Search - {keyword}</h2>
|
||||
<SearchInput {...props} />
|
||||
{postsToShow?.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
return <LayoutBase {...props}>
|
||||
<div className='pb-12'>
|
||||
<SearchInput {...props} />
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
|
||||
{postsToShow.map(p => (
|
||||
<article key={p.id} className="mb-12" >
|
||||
<h2 className="mb-4">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="text-black text-xl md:text-2xl no-underline hover:underline replace"> {p.title}</a>
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700">
|
||||
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href="#" className="text-gray-700">{p.category}</a>
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-normal replace">
|
||||
{p.summary}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { getPageTableOfContents } from 'notion-utils'
|
||||
import LayoutBase from './LayoutBase'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { ArticleInfo } from './components/ArticleInfo'
|
||||
import Comment from '@/components/Comment'
|
||||
|
||||
export const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
@@ -18,57 +17,20 @@ export const LayoutSlug = props => {
|
||||
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
|
||||
|
||||
return (
|
||||
<LayoutBase {...props}>
|
||||
<div>
|
||||
<h2>{post?.title}</h2>
|
||||
<LayoutBase {...props}>
|
||||
|
||||
{lock && <ArticleLock password={post.password} validPassword={validPassword} />}
|
||||
{lock && <ArticleLock password={post.password} validPassword={validPassword} />}
|
||||
|
||||
{!lock && <section id="notion-article" className="px-1">
|
||||
<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">
|
||||
<i className="mr-1 fas fa-folder-open" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className='mr-2'>|</span>
|
||||
{!lock && <div id="notion-article" className="px-2">
|
||||
|
||||
{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>
|
||||
<span className='mx-2 text-gray-400 dark:text-gray-500'>
|
||||
{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedTime}
|
||||
</span>
|
||||
<span className='mr-2'>|</span>
|
||||
{post && <>
|
||||
<ArticleInfo post={post} />
|
||||
<NotionPage post={post} />
|
||||
<Comment frontMatter={post}/>
|
||||
</>}
|
||||
</div>}
|
||||
|
||||
</>)}
|
||||
|
||||
<span className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<i className='mr-1 fas fa-eye' />
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{post && <NotionPage post={post} />}
|
||||
</section>}
|
||||
|
||||
</div>
|
||||
</LayoutBase>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useState } from 'react'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTag = props => {
|
||||
const { tag, posts } = props
|
||||
const { posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
@@ -24,26 +24,36 @@ export const LayoutTag = props => {
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase>
|
||||
Tag - {tag}
|
||||
{postsToShow.map(p => (
|
||||
<div key={p.id} className="border my-12">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="underline cursor-pointer">{p.title}</a>
|
||||
</Link>
|
||||
<div>{p.summary}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
return <LayoutBase>
|
||||
{postsToShow.map(p => (
|
||||
<article key={p.id} className="mb-12" >
|
||||
<h2 className="mb-4">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="text-black text-xl md:text-2xl no-underline hover:underline"> {p.title}</a>
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700">
|
||||
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href="#" className="text-gray-700">{p.category}</a>
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-normal">
|
||||
{p.summary}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<div
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
onClick={handleGetMore}
|
||||
className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBase>
|
||||
)
|
||||
|
||||
</LayoutBase >
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutTagIndex = (props) => {
|
||||
const { tags } = props
|
||||
const { locale } = useGlobal()
|
||||
return <LayoutBase {...props}>
|
||||
<div className='p-10'>
|
||||
<div className='dark:text-gray-200 mb-5'><i className='mr-4 fas fa-tag'/>{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'>
|
||||
<Link key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
|
||||
<div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{tags.map(tag => {
|
||||
return <div key={tag.name} className='p-2'>
|
||||
<Link key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200
|
||||
mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
|
||||
<div className='font-light dark:text-gray-400'><i className='mr-1 fas fa-tag'/> {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
}) }
|
||||
</div>
|
||||
</div> </LayoutBase>
|
||||
<div className='font-light dark:text-gray-400'><i className='mr-1 fas fa-tag' /> {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div> </LayoutBase>
|
||||
}
|
||||
|
||||
47
themes/example/components/ArticleInfo.js
Normal file
47
themes/example/components/ArticleInfo.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
|
||||
export const ArticleInfo = (props) => {
|
||||
const { post } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
|
||||
|
||||
return <section className="flex-wrap flex mt-2 text-gray-400 dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
{post?.type[0] !== 'Page' && <>
|
||||
<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">
|
||||
<i className="mr-1 fas fa-folder-open" />
|
||||
{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>
|
||||
<span className='mx-2 text-gray-400 dark:text-gray-500'>
|
||||
{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedTime}
|
||||
</span>
|
||||
<span className='mr-2'>|</span>
|
||||
<span className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<i className='mr-1 fas fa-eye' />
|
||||
|
||||
<span className="mr-2 busuanzi_value_page_pv" />
|
||||
</span>
|
||||
</>)}
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
}
|
||||
@@ -24,9 +24,9 @@ export const ArticleLock = props => {
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lgfont-light leading-10 text-black bg-white dark:bg-gray-500'></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
@@ -36,5 +36,5 @@ export const ArticleLock = props => {
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
62
themes/example/components/BlogList.js
Normal file
62
themes/example/components/BlogList.js
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const BlogList = (props) => {
|
||||
const { page, posts, postCount } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
const showNext =
|
||||
page < totalPage &&
|
||||
posts.length === BLOG.POSTS_PER_PAGE &&
|
||||
posts.length < postCount
|
||||
|
||||
const currentPage = +page
|
||||
|
||||
return <div className="w-full md:pr-12 mb-12">
|
||||
|
||||
{posts.map(p => (
|
||||
<article key={p.id} className="mb-12" >
|
||||
<h2 className="mb-4">
|
||||
<Link href={`/article/${p.slug}`}>
|
||||
<a className="text-black text-xl md:text-2xl no-underline hover:underline"> {p.title}</a>
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-700">
|
||||
by <a href="#" className="text-gray-700">{BLOG.AUTHOR}</a> on {p.date?.start_date || p.createdTime}
|
||||
<span className="font-bold mx-1"> | </span>
|
||||
<a href="#" className="text-gray-700">{p.category}</a>
|
||||
{/* <span className="font-bold mx-1"> | </span> */}
|
||||
{/* <a href="#" className="text-gray-700">2 Comments</a> */}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 leading-normal">
|
||||
{p.summary}
|
||||
</p>
|
||||
{/* 搜索结果 */}
|
||||
{p.results && (
|
||||
<p className="mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{p.results.map(r => (
|
||||
<span key={r}>{r}</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
|
||||
<div className="flex justify-between text-xs">
|
||||
<Link href={{ pathname: currentPage - 1 === 1 ? `${BLOG.SUB_PATH || '/'}` : `/page/${currentPage - 1}`, query: router.query.s ? { s: router.query.s } : {} }}>
|
||||
<a className={`${currentPage > 1 ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}>{locale.PAGINATION.PREV}</a>
|
||||
</Link>
|
||||
<Link href={{ pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}>
|
||||
<a className={`${showNext ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}>{locale.PAGINATION.NEXT}</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
49
themes/example/components/Footer.js
Normal file
49
themes/example/components/Footer.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export const Footer = (props) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
|
||||
|
||||
// {/* 页脚 */}
|
||||
// <footer className="font-sans dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-sm p-6">
|
||||
// <i className="fas fa-copyright" /> {`${startYear}${currentYear}`}{' '}
|
||||
|
||||
// <br />
|
||||
// <span className="hidden busuanzi_container_site_pv">
|
||||
// <i className="fas fa-eye" />
|
||||
// <span className="px-1 busuanzi_value_site_pv"> </span>{' '}
|
||||
// </span>
|
||||
// <span className="pl-2 hidden busuanzi_container_site_uv">
|
||||
// <i className="fas fa-users" />{' '}
|
||||
// <span className="px-1 busuanzi_value_site_uv"> </span>{' '}
|
||||
// </span>
|
||||
// <br />
|
||||
// <h1>{meta?.title || siteInfo?.title}</h1>
|
||||
// <span className='text-xs font-serif'>
|
||||
// Powered by{' '}
|
||||
// <a
|
||||
// href="https://github.com/tangly1024/NotionNext"
|
||||
// className="underline dark:text-gray-300"
|
||||
// >
|
||||
// NotionNext {BLOG.VERSION}
|
||||
// </a>
|
||||
// .
|
||||
// </span>
|
||||
// </footer>
|
||||
|
||||
return <footer className="w-full bg-white px-6 border-t">
|
||||
<div className="container mx-auto max-w-4xl py-6 flex flex-wrap md:flex-no-wrap justify-between items-center text-sm">
|
||||
©{`${startYear}${currentYear}`} {BLOG.AUTHOR}. All rights reserved.
|
||||
<div className="pt-4 md:p-0 text-center md:text-right text-xs">
|
||||
{/* 右侧链接 */}
|
||||
{/* <a href="#" className="text-black no-underline hover:underline">Privacy Policy</a> */}
|
||||
{BLOG.BEI_AN && (<a href="https://beian.miit.gov.cn/" className="text-black no-underline hover:underline ml-4">{BLOG.BEI_AN} </a>)}
|
||||
<span className='text-black no-underline ml-4'>
|
||||
Powered by
|
||||
<a href="https://github.com/tangly1024/NotionNext" className=' hover:underline'> NotionNext {BLOG.VERSION} </a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
21
themes/example/components/Header.js
Normal file
21
themes/example/components/Header.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 网站顶部
|
||||
* @returns
|
||||
*/
|
||||
export const Header = (props) => {
|
||||
const { siteInfo } = props
|
||||
|
||||
return <header className="w-full px-6 bg-white">
|
||||
<div className="container mx-auto max-w-4xl md:flex justify-between items-center">
|
||||
<Link href='/'>
|
||||
<a className="py-6 w-full text-center md:text-left md:w-auto text-gray-dark no-underline flex justify-center items-center">
|
||||
{siteInfo?.title}
|
||||
</a></Link>
|
||||
<div className="w-full md:w-auto text-center md:text-right">
|
||||
{/* 右侧文字 */}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
19
themes/example/components/JumpToTopButton.js
Normal file
19
themes/example/components/JumpToTopButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param targetRef 关联高度的目标html标签
|
||||
* @param showPercent 是否显示百分比
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = () => {
|
||||
const { locale } = useGlobal()
|
||||
return <div title={locale.POST.TOP} className='cursor-pointer p-2 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
><i className='fas fa-angle-up text-2xl' />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default JumpToTopButton
|
||||
39
themes/example/components/Nav.js
Normal file
39
themes/example/components/Nav.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 菜单导航
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Nav = (props) => {
|
||||
const { customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search' },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive' },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category' },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag' }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
return <nav className="w-full bg-white md:pt-0 px-6 relative z-20 border-t border-b border-gray-light">
|
||||
<div className="container mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start">
|
||||
<div className="w-full md:w-2/3 text-center md:text-left py-4 flex flex-wrap justify-center items-stretch md:justify-start md:items-start">
|
||||
{links.map(link => {
|
||||
return link && <Link href={link.to}>
|
||||
<a className="px-2 md:pl-0 md:mr-3 md:pr-3 text-gray-700 no-underline md:border-r border-gray-light">
|
||||
{link.name}
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
<div className="w-full md:w-1/3 text-center md:text-right">
|
||||
{/* <!-- extra links --> */}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
44
themes/example/components/SideBar.js
Normal file
44
themes/example/components/SideBar.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import Live2D from '@/components/Live2D'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const SideBar = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { latestPosts, categories } = props
|
||||
return <div className="w-full md:w-64 sticky top-8">
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 py-3 px-4 border-b">{locale.COMMON.CATEGORY}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{categories?.map(category => {
|
||||
return <Link key={category.name} href={`/category/${category.name}`} passHref>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
|
||||
</Link>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 py-3 px-4 border-b">{locale.COMMON.LATEST_POSTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{latestPosts?.map(p => {
|
||||
return <Link key={p.id} href={`/article/${p.slug}`} passHref>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||
</Link>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside className="rounded overflow-hidden mb-6">
|
||||
<Live2D />
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
}
|
||||
19
themes/example/components/Title.js
Normal file
19
themes/example/components/Title.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 标题栏
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Title = (props) => {
|
||||
const { siteInfo, post } = props
|
||||
const title = post?.title || siteInfo?.description
|
||||
const description = post?.description || BLOG.AUTHOR
|
||||
|
||||
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 border-b">
|
||||
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
|
||||
<p className="leading-loose text-gray-dark">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const LayoutSearch = (props) => {
|
||||
const currentSearch = keyword || router?.query?.s
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('container')
|
||||
const container = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${currentSearch}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${currentSearch}</span>`)
|
||||
|
||||
@@ -24,11 +24,11 @@ export const ArticleLock = props => {
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className="flex justify-center">
|
||||
return (<div id='container' className="flex justify-center">
|
||||
<div className="shadow md:hover:shadow-2xl overflow-x-auto max-w-5xl w-screen md:w-full py-10 px-5 lg:pt-24 md:px-24 min-h-screen dark:border-gray-700 bg-white dark:bg-gray-800 duration-200 subpixel-antialiased">
|
||||
<div className="w-full flex justify-center items-center h-96 font-sans">
|
||||
<div className="text-center space-y-3">
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className="flex">
|
||||
<input
|
||||
id="password" type='password'
|
||||
|
||||
@@ -25,7 +25,7 @@ function AsideLeft (props) {
|
||||
|
||||
<section className='flex flex-col dark:text-gray-300'>
|
||||
<hr className='w-12 my-8' />
|
||||
{ siteInfo.description }
|
||||
{ siteInfo?.description }
|
||||
</section>
|
||||
|
||||
{router.asPath !== '/tag' && <section className='flex flex-col'>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Layout404 = props => {
|
||||
// 延时3秒如果加载失败就返回首页
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const article = document.getElementById('container')
|
||||
const article = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/').then(() => {
|
||||
console.log('找不到页面', router.asPath)
|
||||
|
||||
@@ -59,15 +59,14 @@ const LayoutBase = props => {
|
||||
|
||||
{headerSlot}
|
||||
|
||||
<main id="wrapper" className="w-full py-8 md:px-8 xl:px-24 min-h-screen">
|
||||
<main id="wrapper" className="w-full py-8 md:px-8 lg:px-24 min-h-screen">
|
||||
<div
|
||||
id="container-inner"
|
||||
className="pt-14 w-full mx-auto lg:flex justify-center lg:space-x-4"
|
||||
className="pt-14 w-full mx-auto lg:flex lg:space-x-4 justify-center"
|
||||
>
|
||||
<div className="flex-grow w-full">
|
||||
<div className="w-full max-w-4xl">
|
||||
{onLoading ? <LoadingCover /> : children}
|
||||
</div>
|
||||
|
||||
<SideRight {...props} slot={rightAreaSlot} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const LayoutSlug = props => {
|
||||
|
||||
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased" >
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-5'>
|
||||
<section id='notion-article' className='px-5 justify-center mx-auto'>
|
||||
{post && <NotionPage post={post} />}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ArticleLock = props => {
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
@@ -35,5 +35,5 @@ export const ArticleLock = props => {
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -103,11 +103,11 @@ const Header = props => {
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${siteInfo.pageCover}")`
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${siteInfo?.pageCover}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex flex-col h-full items-center justify-center w-full font-sans">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo.title}</div>
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed'/>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect } from 'react'
|
||||
|
||||
export default function HeaderArticle({ post, siteInfo }) {
|
||||
if (!post) {
|
||||
return <>loading...</>
|
||||
return <></>
|
||||
}
|
||||
const headerImage = post?.page_cover ? `url("${post.page_cover}")` : `url("${siteInfo?.pageCover}")`
|
||||
const { isDarkMode } = useGlobal()
|
||||
@@ -41,6 +41,9 @@ export default function HeaderArticle({ post, siteInfo }) {
|
||||
if (!isDarkMode) {
|
||||
const stickyNavElement = document.getElementById('sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
if (!header || !stickyNavElement) {
|
||||
return
|
||||
}
|
||||
if (window.scrollY < header.clientHeight) {
|
||||
stickyNavElement?.classList?.add('dark')
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
const Logo = props => {
|
||||
const { siteInfo } = props
|
||||
return <Link href='/' passHref>
|
||||
<div className='flex flex-col justify-center items-center cursor-pointer space-y-3'>
|
||||
<div className='font-sans text-lg p-1.5 rounded bg-black text-white dark:border-white border-black border'> {siteInfo?.title}</div>
|
||||
</div>
|
||||
<div className='flex flex-col justify-center items-center cursor-pointer space-y-3'>
|
||||
<div className='font-sans text-lg p-1.5 rounded bg-black text-white dark:border-white border-black border'> {siteInfo?.title || BLOG.TITLE}</div>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
export default Logo
|
||||
|
||||
@@ -21,7 +21,7 @@ const MenuList = (props) => {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans'>
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-300 font-sans'>
|
||||
{links.map(link => {
|
||||
if (link && link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
|
||||
@@ -9,7 +9,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || document.getElementById('container')
|
||||
const target = currentRef || (typeof document !== 'undefined' && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function SideRight(props) {
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className={'lg:w-80 px-2 space-y-4 pt-4 lg:pt-0'}>
|
||||
<div className={'space-y-4 lg:w-80 lg:pt-0 lg:px-2 pt-4'}>
|
||||
<InfoCard {...props} />
|
||||
{CONFIG_HEXO.WIDGET_ANALYTICS && <AnalyticsCard {...props} />}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ const TopNav = props => {
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'fixed bg-none animate__animated animate__fadeIn dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full top-0 z-20 transform duration-200 font-san border-transparent dark:border-transparent'}>
|
||||
<div id='sticky-nav' className={'top-0 shadow fixed bg-none animate__animated animate__fadeIn dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full z-20 transform duration-200 font-san border-transparent dark:border-transparent'}>
|
||||
<div className='w-full flex justify-between items-center px-4 py-2'>
|
||||
<div className='flex'>
|
||||
<Logo {...props}/>
|
||||
@@ -103,7 +103,7 @@ const TopNav = props => {
|
||||
</div>
|
||||
|
||||
<Collapse isOpen={isOpen} className='shadow-xl'>
|
||||
<div className='bg-white pt-1 py-2 px-5 lg:hidden'>
|
||||
<div className='bg-white dark:bg-hexo-black-gray pt-1 py-2 px-5 lg:hidden '>
|
||||
<MenuList {...props}/>
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
@@ -11,23 +11,23 @@ export const LayoutSearch = (props) => {
|
||||
const { keyword } = props
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('container')
|
||||
const container = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${keyword}</span>`)
|
||||
}
|
||||
},
|
||||
100)
|
||||
100)
|
||||
})
|
||||
return <LayoutBase {...props}>
|
||||
<div className='py-12'>
|
||||
<div className='pb-4 w-full'>{locale.NAV.SEARCH}</div>
|
||||
<SearchInput currentSearch={keyword} {...props}/>
|
||||
<TagGroups {...props}/>
|
||||
<CategoryGroup {...props}/>
|
||||
<SearchInput currentSearch={keyword} {...props} />
|
||||
<TagGroups {...props} />
|
||||
<CategoryGroup {...props} />
|
||||
</div>
|
||||
<div id='container'>
|
||||
<BlogPostListScroll {...props}/>
|
||||
<BlogPostListScroll {...props} />
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ArticleLock = props => {
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 font-sans'>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
@@ -36,5 +36,5 @@ export const ArticleLock = props => {
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || document.getElementById('container')
|
||||
const target = currentRef || (typeof document !== 'undefined' && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Layout404 = props => {
|
||||
// 延时3秒如果加载失败就返回首页
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const article = document.getElementById('container')
|
||||
const article = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (!article) {
|
||||
router.push('/').then(() => {
|
||||
console.log('找不到页面', router.asPath)
|
||||
|
||||
@@ -7,7 +7,7 @@ export const LayoutSearch = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { posts, keyword } = props
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('container')
|
||||
const container = typeof document !== 'undefined' && document.getElementById('container')
|
||||
if (container && container.innerHTML) {
|
||||
const re = new RegExp(`${keyword}`, 'gim')
|
||||
container.innerHTML = container.innerHTML.replace(re, `<span class='text-red-500 border-b border-dashed'>${keyword}</span>`)
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ArticleLock = props => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full py-10 px-5 lg:pt-24 md:px-24 min-h-screen dark:border-gray-700 bg-white dark:bg-gray-800 duration-200">
|
||||
<div id='container' className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full py-10 px-5 lg:pt-24 md:px-24 min-h-screen dark:border-gray-700 bg-white dark:bg-gray-800 duration-200">
|
||||
<div className="w-full flex justify-center items-center h-96 font-sans">
|
||||
<div className="text-center space-y-3">
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function Header(props) {
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${siteInfo.pageCover}")`
|
||||
`linear-gradient(rgba(0, 0, 0, 0.8), rgba(0,0,0,0.2), rgba(0, 0, 0, 0.8) ),url("${siteInfo?.pageCover}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex h-full items-center lg:-mt-14 justify-center w-full text-4xl md:text-7xl text-white">
|
||||
|
||||
@@ -9,7 +9,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || document.getElementById('container')
|
||||
const target = currentRef || (typeof document !== 'undefined' && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
|
||||
Reference in New Issue
Block a user